// run.ts: run one trigger of a val the way Val Town did. // // run.ts http serve $VAL_HTTP on $PORT (default 8000) // run.ts cron call $VAL_CRON once with { lastRunAt } // run.ts email call $VAL_EMAIL with the message in // // It runs in the val's directory, which holds its code, deno.json, .env, // and data/. The handler is the file's default export, or its only export. import PostalMime from "npm:postal-mime@4"; const [mode, arg] = Deno.args; const dir = Deno.cwd(); async function handler(variable: string) { const file = Deno.env.get(variable); if (!file) throw new Error(`${variable} is not set in ${dir}/.env`); const mod = await import(new URL(file, `file://${dir}/`).href); const exported = mod.default ?? Object.values(mod).find((v) => typeof v === "function"); if (typeof exported === "function") return exported; if (typeof exported?.fetch === "function") return exported.fetch.bind(exported); throw new Error(`${file} has no default export to run`); } if (mode === "http") { const fn = await handler("VAL_HTTP"); Deno.serve({ port: Number(Deno.env.get("PORT") ?? 8000) }, (req) => { // Give the val its public host, as on val.run, rather than localhost. // Behind front.ts at //, the val still sees its paths from /, and // the prefix arrives in the X-Forwarded-Prefix header. const host = req.headers.get("x-forwarded-host"); if (host) { const proto = req.headers.get("x-forwarded-proto") ?? "https"; const u = new URL(req.url); req = new Request(`${proto}://${host}${u.pathname}${u.search}`, req); } return fn(req); }); } else if (mode === "cron") { const fn = await handler("VAL_CRON"); await Deno.mkdir(`${dir}/data`, { recursive: true }); const state = `${dir}/data/cron-last-run`; let lastRunAt: Date | undefined; try { lastRunAt = new Date((await Deno.readTextFile(state)).trim()); } catch { /* first run */ } const started = new Date(); await fn({ lastRunAt }); await Deno.writeTextFile(state, started.toISOString() + "\n"); Deno.exit(0); } else if (mode === "email" && arg) { const fn = await handler("VAL_EMAIL"); const m = await PostalMime.parse(await Deno.readFile(arg)); const addr = (a?: { name?: string; address?: string }) => a?.name ? `${a.name} <${a.address}>` : a?.address ?? ""; await fn({ from: addr(m.from), to: (m.to ?? []).map(addr), cc: m.cc?.map(addr), bcc: m.bcc?.map(addr), subject: m.subject, text: m.text, html: m.html, attachments: m.attachments.map((a) => new File([a.content], a.filename ?? "attachment", { type: a.mimeType }) ), headers: Object.fromEntries(m.headers.map((h) => [h.key, h.value])), }); Deno.exit(0); } else { console.error("usage: run.ts http | cron | email "); Deno.exit(2); }