@fastly/js-compute
Version:
JavaScript SDK and CLI for building JavaScript applications on [Fastly Compute](https://www.fastly.com/products/edge-compute/serverless).
201 lines (168 loc) • 5.59 kB
JavaScript
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
let _traceMap;
function getTraceMap() {
if (!_traceMap) {
_traceMap = new TraceMap(globalThis.__FASTLY_SOURCE_MAP);
}
return _traceMap;
}
function buildErrorHeading(e) {
const name = e?.name || 'Error';
// Prefer e.cause.message if message is missing and cause is informative
const msg =
(typeof e?.message === 'string' && e.message) ||
(e?.cause && typeof e.cause?.message === 'string' && e.cause.message) ||
'';
return msg ? `${name}: ${msg}` : name;
}
function getSourceContext(source, line, col, { radius = 3 } = {}) {
const tm = getTraceMap();
if (!tm) return null;
const idx = tm.sources.indexOf(source);
if (idx < 0) return null;
const content = tm.sourcesContent?.[idx];
if (!content) return null; // gracefully skip
const lines = content.split('\n');
const start = Math.max(0, line - 1 - radius);
const end = Math.min(lines.length - 1, line - 1 + radius);
const result = [];
for (let i = start; i <= end; i++) {
const prefix = i === (line - 1) ? '>' : ' ';
const num = String(i + 1).padStart(5);
result.push(`${prefix} ${num} | ${lines[i]}`);
if (i === line - 1) {
result.push(` ${''.padStart(col)}^`);
}
}
return result;
}
function parseStarlingMonkeyFrame(line) {
line = String(line).trim().replace(/\)?$/, ""); // strip trailing ')'
const m = line.match(/:(\d+):(\d+)\s*$/);
if (!m) return null;
const col = +m[2];
const lineNo = +m[1];
const head = line.slice(0, m.index);
if (head.startsWith('@')) { // no function name
return { fn: null, file: head.slice(1), line: lineNo, col, };
}
const at = head.lastIndexOf("@");
if (at > 0) { // fn@file
return { fn: head.slice(0, at), file: head.slice(at + 1), line: lineNo, col, };
}
// file only
return { fn: null, file: head, line: lineNo, col, };
}
function mapStack(raw) {
const tm = getTraceMap();
const out = [];
const lines = String(raw).split(/\r?\n/);
for (const line of lines) {
if (line.startsWith('node_modules/@fastly/js-compute/rsrc/trace-mapping.inject.js')) {
// If the line comes from this file, skip it
continue;
}
if (line === '') { out.push({l: line}); continue; }
const m = parseStarlingMonkeyFrame(line);
if (!m) { out.push({e:'(frame not parsed)', l:line}); continue; }
const { fn, file, line: l, col: c } = m;
const genLine = Number(l);
const genCol = Number(c);
// Only map frames that come from the generated bundle
if (file !== globalThis.__FASTLY_GEN_FILE) { out.push({e:'(frame not mapped)', l:line}); continue; }
const pos = originalPositionFor(tm, { line: genLine, column: Math.max(0, genCol - 1) });
if (!pos?.source) {
continue;
}
out.push({m:{pos,fn}, l: line});
}
return out;
}
function mapError(e) {
const lines = [];
const raw = e?.stack ?? String(e);
lines.push(buildErrorHeading(e));
try {
const stack = mapStack(raw);
let contextOutputted = false;
for (const line of stack) {
const { e, m, l } = line;
if (l === '') {
lines.push('');
continue;
}
if (e != null) {
lines.push(`${e} ${l}`);
continue;
}
let formatted;
let ctx;
if (m == null) {
formatted = l;
} else {
const {pos, fn} = m;
let name = pos.name;
if (fn == null || fn === '') {
name = null;
} else if (fn[fn.length-1] === '<') {
name = '(anonymous function)';
} else {
name = fn;
}
const filePos = `${pos.source}:${pos.line}:${pos.column != null ? pos.column + 1 : 0}`;
formatted = name ? `${name} (${filePos})` : filePos;
if (!contextOutputted) {
ctx = getSourceContext(pos.source, pos.line, pos.column);
contextOutputted = true;
}
}
lines.push(` at ${formatted}`);
if (ctx) {
lines.push(...ctx);
}
}
} catch {
lines.push('(Raw error)');
lines.push(raw);
}
return lines;
}
// Monkey patch addEventListener('fetch')
const _orig_addEventListener = globalThis.addEventListener;
globalThis.addEventListener = function (type, listener) {
if (type !== 'fetch') {
return _orig_addEventListener.call(this, type, listener);
}
const _orig_listener = listener;
return _orig_addEventListener.call(this, type, (event) => {
// Patch respondWith on this event instance
const _orig_respondWith = event.respondWith.bind(event);
event.respondWith = (value) => {
const wrappedValue = Promise
.resolve(value)
.catch((err) => {
console.error('Unhandled error while running request handler');
try { globalThis.__fastlyMapAndLogError(err); } catch { /* swallow */ }
console.error('Raw error below:');
throw err;
});
try {
return _orig_respondWith(wrappedValue);
} catch (err) {
console.error('Unhandled error sending response');
try { globalThis.__fastlyMapAndLogError(err); } catch { /* swallow */ }
console.error('Raw error below:');
throw err;
}
};
try {
return _orig_listener.call(this, event);
} catch (err) {
console.error('Unhandled error running event listener');
try { globalThis.__fastlyMapAndLogError(err); } catch { /* swallow */ }
console.error('Raw error below:');
throw err;
}
});
};
globalThis.__fastlyMapError = mapError