MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add og title card

+394 -64
docs/reports/ant.lockb

This is a binary file and will not be displayed.

+8 -2
docs/reports/migrations/0000_conscious_vampiro.sql docs/reports/migrations/0000_melodic_maginty.sql
··· 1 + CREATE TABLE `frames` ( 2 + `hash` text PRIMARY KEY NOT NULL, 3 + `frame` text NOT NULL 4 + ); 5 + --> statement-breakpoint 1 6 CREATE TABLE `report_frames` ( 2 7 `report_id` text NOT NULL, 3 8 `frame_index` integer NOT NULL, 4 - `frame` text NOT NULL, 9 + `frame_hash` text NOT NULL, 5 10 PRIMARY KEY(`report_id`, `frame_index`), 6 - FOREIGN KEY (`report_id`) REFERENCES `reports`(`id`) ON UPDATE no action ON DELETE cascade 11 + FOREIGN KEY (`report_id`) REFERENCES `reports`(`id`) ON UPDATE no action ON DELETE cascade, 12 + FOREIGN KEY (`frame_hash`) REFERENCES `frames`(`hash`) ON UPDATE no action ON DELETE restrict 7 13 ); 8 14 --> statement-breakpoint 9 15 CREATE TABLE `reports` (
+40 -3
docs/reports/migrations/meta/0000_snapshot.json
··· 1 1 { 2 2 "version": "6", 3 3 "dialect": "sqlite", 4 - "id": "74f454aa-26ab-4ce9-816d-b14eec7662d2", 4 + "id": "f888823f-8746-428a-894d-7a6e8578f8d2", 5 5 "prevId": "00000000-0000-0000-0000-000000000000", 6 6 "tables": { 7 + "frames": { 8 + "name": "frames", 9 + "columns": { 10 + "hash": { 11 + "name": "hash", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "frame": { 18 + "name": "frame", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + } 24 + }, 25 + "indexes": {}, 26 + "foreignKeys": {}, 27 + "compositePrimaryKeys": {}, 28 + "uniqueConstraints": {}, 29 + "checkConstraints": {} 30 + }, 7 31 "report_frames": { 8 32 "name": "report_frames", 9 33 "columns": { ··· 21 45 "notNull": true, 22 46 "autoincrement": false 23 47 }, 24 - "frame": { 25 - "name": "frame", 48 + "frame_hash": { 49 + "name": "frame_hash", 26 50 "type": "text", 27 51 "primaryKey": false, 28 52 "notNull": true, ··· 42 66 "id" 43 67 ], 44 68 "onDelete": "cascade", 69 + "onUpdate": "no action" 70 + }, 71 + "report_frames_frame_hash_frames_hash_fk": { 72 + "name": "report_frames_frame_hash_frames_hash_fk", 73 + "tableFrom": "report_frames", 74 + "tableTo": "frames", 75 + "columnsFrom": [ 76 + "frame_hash" 77 + ], 78 + "columnsTo": [ 79 + "hash" 80 + ], 81 + "onDelete": "restrict", 45 82 "onUpdate": "no action" 46 83 } 47 84 },
+2 -2
docs/reports/migrations/meta/_journal.json
··· 5 5 { 6 6 "idx": 0, 7 7 "version": "6", 8 - "when": 1777152332594, 9 - "tag": "0000_conscious_vampiro", 8 + "when": 1777160084343, 9 + "tag": "0000_melodic_maginty", 10 10 "breakpoints": true 11 11 } 12 12 ]
+2 -1
docs/reports/package.json
··· 12 12 "hono": "^4.12.15", 13 13 "drizzle-orm": "^0.45.2", 14 14 "short-uuid": "^6.0.3", 15 - "zod": "^4.3.6" 15 + "zod": "^4.3.6", 16 + "@resvg/resvg-wasm": "^2.6.2" 16 17 }, 17 18 "devDependencies": { 18 19 "wrangler": "^4.4.0",
docs/reports/public/assets/Arial-Bold.ttf

This is a binary file and will not be displayed.

docs/reports/public/assets/Arial.ttf

This is a binary file and will not be displayed.

+27
docs/reports/src/format.ts
··· 1 + export function formatBytes(value: number | null): string { 2 + if (!value || !Number.isFinite(value)) return 'unknown'; 3 + if (value >= 1024 * 1024) return `${Math.round(value / 1024 / 1024)}mb`; 4 + if (value >= 1024) return `${Math.round(value / 1024)}kb`; 5 + return `${value}b`; 6 + } 7 + 8 + export function crashDetail(code: string): string { 9 + switch (code) { 10 + case 'SIGSEGV': 11 + case 'EXCEPTION_ACCESS_VIOLATION': 12 + return 'Invalid memory access'; 13 + case 'SIGBUS': 14 + return 'Bus error'; 15 + case 'SIGFPE': 16 + return 'Floating point exception'; 17 + case 'SIGILL': 18 + case 'EXCEPTION_ILLEGAL_INSTRUCTION': 19 + return 'Illegal instruction'; 20 + case 'SIGABRT': 21 + return 'Abort'; 22 + case 'EXCEPTION_STACK_OVERFLOW': 23 + return 'Stack overflow'; 24 + default: 25 + return 'Fatal error'; 26 + } 27 + }
+121 -18
docs/reports/src/index.ts
··· 1 1 import { Hono } from 'hono'; 2 + import { renderOgPng } from './og'; 2 3 import { asc, eq } from 'drizzle-orm'; 3 4 import { drizzle } from 'drizzle-orm/d1'; 4 5 import { renderBlank, renderReport } from './view'; 5 6 import { generate as generateShortUuid } from 'short-uuid'; 6 - import { CrashReportSchema, reportFrames, reports, type CrashReport } from './schema'; 7 7 8 - type Bindings = { DB: D1Database }; 8 + import { 9 + CrashReportSchema, 10 + frames as frameTable, 11 + reportFrames, 12 + reports, 13 + type CrashReport, 14 + } from './schema'; 15 + 16 + type Bindings = { ASSETS: Fetcher; DB: D1Database }; 9 17 const app = new Hono<{ Bindings: Bindings }>(); 18 + 19 + let antLogoDataUrl: Promise<string> | null = null; 20 + let ogFontBytes: Promise<Uint8Array[]> | null = null; 10 21 11 22 app.get('/', c => c.html(renderBlank(), 404)); 12 23 ··· 23 34 frames: report.frames, 24 35 }); 25 36 26 - const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(traceInput)); 37 + return sha256(traceInput); 38 + } 39 + 40 + async function sha256(input: string): Promise<string> { 41 + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); 27 42 return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join(''); 28 43 } 29 44 30 45 function publicUrl(requestUrl: string, id: string): string { 31 46 const url = new URL(requestUrl); 32 47 return `${url.origin}/${id}`; 48 + } 49 + 50 + function publicOgImageUrl(requestUrl: string, id: string): string { 51 + const url = new URL(requestUrl); 52 + return `${url.origin}/og/${id}.png`; 53 + } 54 + 55 + function bytesToBase64(bytes: Uint8Array): string { 56 + let binary = ''; 57 + const chunkSize = 0x8000; 58 + for (let i = 0; i < bytes.length; i += chunkSize) { 59 + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); 60 + } 61 + return btoa(binary); 62 + } 63 + 64 + async function loadAntLogoDataUrl(env: Bindings, requestUrl: string): Promise<string> { 65 + const response = await env.ASSETS.fetch(new Request(new URL('/assets/ant.png', requestUrl))); 66 + if (!response.ok) return ''; 67 + const bytes = new Uint8Array(await response.arrayBuffer()); 68 + return `data:image/png;base64,${bytesToBase64(bytes)}`; 69 + } 70 + 71 + async function loadAssetBytes( 72 + env: Bindings, 73 + requestUrl: string, 74 + path: string, 75 + ): Promise<Uint8Array> { 76 + const response = await env.ASSETS.fetch(new Request(new URL(path, requestUrl))); 77 + if (!response.ok) return new Uint8Array(); 78 + return new Uint8Array(await response.arrayBuffer()); 79 + } 80 + 81 + function getAntLogoDataUrl(env: Bindings, requestUrl: string): Promise<string> { 82 + antLogoDataUrl ??= loadAntLogoDataUrl(env, requestUrl); 83 + return antLogoDataUrl; 84 + } 85 + 86 + function ogImageId(value: string): string { 87 + return value.endsWith('.png') ? value.slice(0, -4) : value; 88 + } 89 + 90 + function getOgFontBytes(env: Bindings, requestUrl: string): Promise<Uint8Array[]> { 91 + ogFontBytes ??= Promise.all([ 92 + loadAssetBytes(env, requestUrl, '/assets/Arial.ttf'), 93 + loadAssetBytes(env, requestUrl, '/assets/Arial-Bold.ttf'), 94 + loadAssetBytes(env, requestUrl, '/assets/BerkeleyMono-Regular.woff2'), 95 + ]); 96 + return ogFontBytes; 33 97 } 34 98 35 99 const reportFromRows = ( 36 100 row: typeof reports.$inferSelect, 37 - frames: (typeof reportFrames.$inferSelect)[], 101 + frameRows: { frame: string }[], 38 102 ): CrashReport => ({ 39 103 schema: 1, 40 104 runtime: 'ant', ··· 48 112 addr: row.faultAddress, 49 113 elapsedMs: row.elapsedMs, 50 114 peakRss: row.peakRss, 51 - frames: frames.map(frame => frame.frame), 115 + frames: frameRows.map(row => row.frame), 52 116 }); 53 117 54 118 async function insertReportFrames( ··· 57 121 frames: string[], 58 122 ): Promise<void> { 59 123 if (!frames.length) return; 124 + const frameHashes = await Promise.all(frames.map(frame => sha256(frame))); 125 + const uniqueFrames = new Map<string, string>(); 126 + 127 + frames.forEach((frame, index) => { 128 + uniqueFrames.set(frameHashes[index], frame); 129 + }); 130 + 60 131 await db 61 - .insert(reportFrames) 62 - .values(frames.map((frame, index) => ({ frame, reportId, frameIndex: index }))); 132 + .insert(frameTable) 133 + .values([...uniqueFrames].map(([hash, frame]) => ({ hash, frame }))) 134 + .onConflictDoNothing(); 135 + 136 + await db.insert(reportFrames).values( 137 + frameHashes.map((frameHash, index) => ({ 138 + frameHash, 139 + reportId, 140 + frameIndex: index, 141 + })), 142 + ); 63 143 } 64 144 65 145 async function getReportFrames( 66 146 db: ReturnType<typeof drizzle>, 67 147 reportId: string, 68 - ): Promise<(typeof reportFrames.$inferSelect)[]> { 148 + ): Promise<{ frame: string }[]> { 69 149 return db 70 - .select() 150 + .select({ 151 + frame: frameTable.frame, 152 + }) 71 153 .from(reportFrames) 154 + .innerJoin(frameTable, eq(reportFrames.frameHash, frameTable.hash)) 72 155 .where(eq(reportFrames.reportId, reportId)) 73 156 .orderBy(asc(reportFrames.frameIndex)); 74 157 } 75 158 76 - app.get('/:id', async c => { 159 + async function getReport(db: ReturnType<typeof drizzle>, id: string): Promise<CrashReport | null> { 160 + const [row] = await db.select().from(reports).where(eq(reports.id, id)).limit(1); 161 + 162 + if (!row) return null; 163 + const frames = await getReportFrames(db, row.id); 164 + return reportFromRows(row, frames); 165 + } 166 + 167 + app.get('/og/:image', async c => { 77 168 const db = drizzle(c.env.DB); 169 + const id = ogImageId(c.req.param('image')); 170 + const report = await getReport(db, id); 171 + if (!report) return c.notFound(); 78 172 79 - const [row] = await db 80 - .select() 81 - .from(reports) 82 - .where(eq(reports.id, c.req.param('id'))) 83 - .limit(1); 173 + const png = await renderOgPng( 174 + report, 175 + await getAntLogoDataUrl(c.env, c.req.url), 176 + await getOgFontBytes(c.env, c.req.url), 177 + ); 84 178 85 - if (!row) return c.html(renderBlank(), 404); 86 - const frames = await getReportFrames(db, row.id); 179 + return c.body(png, 200, { 180 + 'Content-Type': 'image/png', 181 + 'Cache-Control': 'public, max-age=3600', 182 + }); 183 + }); 184 + 185 + app.get('/:id', async c => { 186 + const db = drizzle(c.env.DB); 187 + const id = c.req.param('id'); 188 + const report = await getReport(db, id); 87 189 88 - return c.html(renderReport(reportFromRows(row, frames), publicUrl(c.req.url, row.id))); 190 + if (!report) return c.html(renderBlank(), 404); 191 + return c.html(renderReport(report, publicUrl(c.req.url, id), publicOgImageUrl(c.req.url, id))); 89 192 }); 90 193 91 194 app.post('/report', async c => {
+124
docs/reports/src/og.ts
··· 1 + import { html, raw } from 'hono/html'; 2 + import { crashDetail } from './format'; 3 + import type { CrashReport } from './schema'; 4 + import { initWasm, Resvg } from '@resvg/resvg-wasm'; 5 + import resvgWasm from '@resvg/resvg-wasm/index_bg.wasm'; 6 + 7 + let resvgReady: Promise<void> | null = null; 8 + 9 + function ensureResvg(): Promise<void> { 10 + resvgReady ??= initWasm(resvgWasm); 11 + return resvgReady; 12 + } 13 + 14 + function truncate(value: string, maxLength: number): string { 15 + if (value.length <= maxLength) return value; 16 + return `${value.slice(0, Math.max(0, maxLength - 3))}...`; 17 + } 18 + 19 + function text(x: number, y: number, value: string, className = ''): string { 20 + if (className) return String(html`<text x="${x}" y="${y}" class="${className}">${value}</text>`); 21 + return String(html`<text x="${x}" y="${y}">${value}</text>`); 22 + } 23 + 24 + function renderOgSvg(report: CrashReport, logoDataUrl: string): string { 25 + const detail = crashDetail(report.code); 26 + const frames = report.frames.slice(0, 8); 27 + const frameLines = frames.length ? frames : ['No native frames were captured.']; 28 + 29 + const frameSvg = frameLines 30 + .map((frame, index) => { 31 + const y = 402 + index * 28; 32 + return [ 33 + text(46, y, `${index + 1}.`, 'frame-index'), 34 + text(94, y, truncate(frame, 96), 'frame'), 35 + ].join(''); 36 + }) 37 + .join(''); 38 + 39 + return String( 40 + html`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630"> 41 + <style> 42 + text { 43 + font-family: Arial; 44 + fill: #222; 45 + } 46 + .muted { 47 + fill: #777; 48 + } 49 + .title { 50 + font-size: 40px; 51 + font-weight: 700; 52 + } 53 + .subtitle { 54 + font-size: 34px; 55 + } 56 + .meta { 57 + font-size: 30px; 58 + } 59 + .label { 60 + fill: #777; 61 + } 62 + .code { 63 + font-family: 'Berkeley Mono'; 64 + font-size: 22px; 65 + } 66 + .frame { 67 + font-family: 'Berkeley Mono'; 68 + font-size: 22px; 69 + } 70 + .frame-index { 71 + font-family: 'Berkeley Mono'; 72 + font-size: 22px; 73 + fill: #999; 74 + } 75 + </style> 76 + <defs> 77 + <linearGradient id="backtrace-fade" x1="0" y1="0" x2="0" y2="1"> 78 + <stop offset="0" stop-color="#fff" stop-opacity="0" /> 79 + <stop offset="1" stop-color="#fff" /> 80 + </linearGradient> 81 + </defs> 82 + <rect width="1200" height="630" fill="#fff" /> 83 + <image href="${logoDataUrl}" x="46" y="32" width="65" height="65" /> 84 + ${text(46, 149, `${report.reason}.`, 'title')} 85 + ${text(46, 196, `${detail} at ${report.addr}`, 'subtitle muted')} 86 + ${text(46, 256, 'Runtime:', 'meta label')} 87 + ${text(196, 256, `Ant ${truncate(report.version, 40)}`, 'meta')} 88 + ${text(46, 294, 'Platform:', 'meta label')} 89 + ${text(196, 294, `${truncate(report.os, 36)} ${report.arch}`, 'meta')} 90 + ${text(46, 352, 'Native backtrace:', 'meta')} ${raw(frameSvg)} 91 + <rect x="46" y="540" width="1108" height="64" fill="url(#backtrace-fade)" /> 92 + </svg>`, 93 + ); 94 + } 95 + 96 + export async function renderOgPng( 97 + report: CrashReport, 98 + logoDataUrl: string, 99 + fontBytes: Uint8Array[], 100 + ): Promise<ArrayBuffer> { 101 + await ensureResvg(); 102 + 103 + const resvg = new Resvg(renderOgSvg(report, logoDataUrl), { 104 + font: { 105 + fontBuffers: fontBytes, 106 + loadSystemFonts: false, 107 + defaultFontFamily: 'Arial', 108 + sansSerifFamily: 'Arial', 109 + monospaceFamily: 'Berkeley Mono', 110 + }, 111 + }); 112 + 113 + try { 114 + const image = resvg.render(); 115 + try { 116 + const png = image.asPng(); 117 + return new Uint8Array(png).buffer; 118 + } finally { 119 + image.free(); 120 + } 121 + } finally { 122 + resvg.free(); 123 + } 124 + }
+8 -1
docs/reports/src/schema.ts
··· 47 47 expiresAt: text('expires_at').notNull(), 48 48 }); 49 49 50 + export const frames = sqliteTable('frames', { 51 + hash: text('hash').primaryKey(), 52 + frame: text('frame').notNull(), 53 + }); 54 + 50 55 export const reportFrames = sqliteTable( 51 56 'report_frames', 52 57 { ··· 54 59 .notNull() 55 60 .references(() => reports.id, { onDelete: 'cascade' }), 56 61 frameIndex: integer('frame_index').notNull(), 57 - frame: text('frame').notNull(), 62 + frameHash: text('frame_hash') 63 + .notNull() 64 + .references(() => frames.hash, { onDelete: 'restrict' }), 58 65 }, 59 66 table => [primaryKey({ columns: [table.reportId, table.frameIndex] })], 60 67 );
+58 -37
docs/reports/src/view.tsx
··· 1 - import type { Child } from 'hono/jsx'; 2 1 import type { CrashReport } from './schema'; 3 - 4 - function formatBytes(value: number | null): string { 5 - if (!value || !Number.isFinite(value)) return 'unknown'; 6 - if (value >= 1024 * 1024) return `${Math.round(value / 1024 / 1024)}mb`; 7 - if (value >= 1024) return `${Math.round(value / 1024)}kb`; 8 - return `${value}b`; 9 - } 10 - 11 - function crashDetail(code: string): string { 12 - switch (code) { 13 - case 'SIGSEGV': 14 - case 'EXCEPTION_ACCESS_VIOLATION': 15 - return 'Invalid memory access'; 16 - case 'SIGBUS': 17 - return 'Bus error'; 18 - case 'SIGFPE': 19 - return 'Floating point exception'; 20 - case 'SIGILL': 21 - case 'EXCEPTION_ILLEGAL_INSTRUCTION': 22 - return 'Illegal instruction'; 23 - case 'SIGABRT': 24 - return 'Abort'; 25 - case 'EXCEPTION_STACK_OVERFLOW': 26 - return 'Stack overflow'; 27 - default: 28 - return 'Fatal error'; 29 - } 30 - } 2 + import { Fragment, type Child } from 'hono/jsx'; 3 + import { crashDetail, formatBytes } from './format'; 31 4 32 5 function renderFrames(frames: string[]): Child { 33 6 if (!frames.length) return 'No native frames were captured.'; 34 7 return frames.map((frame, index) => ( 35 - <> 8 + <Fragment> 36 9 <span class="frame-index">{index + 1}.</span> {frame} 37 10 {index < frames.length - 1 ? '\n' : ''} 38 - </> 11 + </Fragment> 39 12 )); 40 13 } 41 14 42 - const Shell = ({ title, children }: { title: string; children: Child }) => ( 15 + type Meta = { 16 + url: string; 17 + title: string; 18 + description: string; 19 + image?: string; 20 + }; 21 + 22 + const Shell = ({ title, meta, children }: { title: string; meta: Meta; children: Child }) => ( 43 23 <html lang="en"> 44 24 <head> 45 25 <meta charset="utf-8" /> 46 26 <meta name="viewport" content="width=device-width,initial-scale=1" /> 27 + <meta property="og:url" content={meta.url} /> 28 + <meta property="og:type" content="website" /> 29 + <meta property="og:title" content={meta.title} /> 30 + <meta property="og:description" content={meta.description} /> 31 + {meta.image ? ( 32 + <> 33 + <meta property="og:image" content={meta.image} /> 34 + <meta property="og:image:type" content="image/png" /> 35 + <meta property="og:image:width" content="1200" /> 36 + <meta property="og:image:height" content="630" /> 37 + </> 38 + ) : null} 39 + <meta name="twitter:card" content="summary_large_image" /> 40 + <meta property="twitter:domain" content="js.report" /> 41 + <meta property="twitter:url" content={meta.url} /> 42 + <meta name="twitter:title" content={meta.title} /> 43 + <meta name="twitter:description" content={meta.description} /> 44 + {meta.image ? <meta name="twitter:image" content={meta.image} /> : null} 47 45 <link rel="icon" type="image/x-icon" href="/favicon.ico" /> 48 46 <link rel="stylesheet" href="/assets/report.css" /> 49 47 <script src="/assets/report.js" defer></script> ··· 68 66 ); 69 67 70 68 const BlankPage = () => ( 71 - <Shell title="js.report"> 69 + <Shell 70 + title="js.report" 71 + meta={{ 72 + url: 'https://js.report', 73 + title: 'js.report', 74 + description: 'Crash reports for JavaScript runtimes.', 75 + }} 76 + > 72 77 <Logo /> 73 78 <p> 74 79 <b>404.</b> <ins>That's an error.</ins> ··· 80 85 </Shell> 81 86 ); 82 87 83 - const ReportPage = ({ report, url }: { report: CrashReport; url: string }) => { 88 + const ReportPage = ({ 89 + report, 90 + url, 91 + imageUrl, 92 + }: { 93 + report: CrashReport; 94 + url: string; 95 + imageUrl: string; 96 + }) => { 84 97 const detail = crashDetail(report.code); 85 98 86 99 return ( 87 - <Shell title={`Ant crash report | ${detail}`}> 100 + <Shell 101 + title={`Ant crash report | ${detail}`} 102 + meta={{ 103 + url, 104 + title: `${report.reason}. ${detail} at ${report.addr}`, 105 + description: `Ant ${report.version} crashed on ${report.os} ${report.arch}.`, 106 + image: imageUrl, 107 + }} 108 + > 88 109 <Logo /> 89 110 <p> 90 111 <b>{report.reason}.</b>{' '} ··· 137 158 return `<!doctype html>${(<BlankPage />)}`; 138 159 } 139 160 140 - export function renderReport(report: CrashReport, url: string): string { 141 - return `<!doctype html>${(<ReportPage report={report} url={url} />)}`; 161 + export function renderReport(report: CrashReport, url: string, imageUrl: string): string { 162 + return `<!doctype html>${(<ReportPage report={report} url={url} imageUrl={imageUrl} />)}`; 142 163 }
+4
docs/reports/src/wasm.d.ts
··· 1 + declare module '*.wasm' { 2 + const module: WebAssembly.Module; 3 + export default module; 4 + }