forked from
juprodh.me/lichen.wiki
🌿 Collaborative wiki on ATProto
1/**
2 * dev-full.ts — Start a local PDS + appview + firehose, all wired together.
3 *
4 * The PDS runs under Node (better-sqlite3 native addon requirement).
5 * Appview and firehose run under Bun with watch mode.
6 *
7 * Usage: bun run scripts/dev-full.ts
8 */
9
10import { resolve } from "node:path";
11import { type Subprocess, spawn } from "bun";
12
13const children: Subprocess[] = [];
14
15async function main() {
16 console.log("Building CSS (watch mode)...");
17 const css = spawn({
18 cmd: [
19 "bunx",
20 "@tailwindcss/cli",
21 "-i",
22 "public/style.css",
23 "-o",
24 "public/dist.css",
25 "--watch",
26 ],
27 stdout: "inherit",
28 stderr: "inherit",
29 });
30 children.push(css);
31
32 console.log("Starting test PDS (via Node)...");
33
34 const pdsScript = resolve(import.meta.dir, "start-pds.mjs");
35 const pdsProc = spawn({
36 cmd: ["node", pdsScript],
37 stdout: "pipe",
38 stderr: "inherit",
39 });
40 children.push(pdsProc);
41
42 // Read PDS info from stdout
43 const reader = pdsProc.stdout.getReader();
44 let buffer = "";
45 let pdsInfo: {
46 pdsUrl: string;
47 plcUrl: string;
48 accounts: Record<string, { did: string; handle: string; password: string }>;
49 };
50
51 while (true) {
52 const { value, done } = await reader.read();
53 if (done) {
54 throw new Error("PDS process exited before producing output");
55 }
56 buffer += new TextDecoder().decode(value);
57 const newlineIdx = buffer.indexOf("\n");
58 if (newlineIdx !== -1) {
59 pdsInfo = JSON.parse(buffer.slice(0, newlineIdx));
60 reader.releaseLock();
61 break;
62 }
63 }
64
65 const { pdsUrl, plcUrl, accounts } = pdsInfo as {
66 pdsUrl: string;
67 plcUrl: string;
68 accounts: Record<string, { did: string; handle: string; password: string }>;
69 };
70
71 const env: Record<string, string> = {
72 ...(process.env as Record<string, string>),
73 PUBLIC_URL: "http://localhost:3000",
74 RELAY_URL: pdsUrl.replace("http://", "ws://"),
75 HANDLE_RESOLVER_URL: pdsUrl,
76 DEV_PDS_URL: pdsUrl,
77 DEV_PLC_URL: plcUrl,
78 DEV_ACCOUNTS: JSON.stringify(accounts),
79 SEED_DB: "1",
80 DB_PATH: "lichen-dev.db",
81 };
82
83 console.log("Starting appview...");
84 const appview = spawn({
85 cmd: ["bun", "run", "--watch", "src/server/index.ts"],
86 env,
87 stdout: "inherit",
88 stderr: "inherit",
89 });
90 children.push(appview);
91
92 // Wait for appview to initialize the DB schema before starting firehose
93 await new Promise((resolve) => setTimeout(resolve, 1000));
94
95 console.log("Starting firehose subscriber...");
96 const firehose = spawn({
97 cmd: ["bun", "run", "--watch", "src/firehose/index.ts"],
98 env,
99 stdout: "inherit",
100 stderr: "inherit",
101 });
102 children.push(firehose);
103
104 console.log(`\n${"=".repeat(60)}`);
105 console.log("dev-full environment running!");
106 console.log("=".repeat(60));
107 console.log(` PDS: ${pdsUrl}`);
108 console.log(` Appview: http://localhost:3000`);
109 console.log(` Firehose: ${pdsUrl.replace("http://", "ws://")}`);
110 console.log("");
111 console.log(" Test accounts:");
112 for (const [_name, acct] of Object.entries(accounts)) {
113 console.log(` ${acct.handle} (${acct.did})`);
114 }
115 console.log("");
116 console.log(" Dev login:");
117 for (const acct of Object.values(accounts)) {
118 console.log(` http://localhost:3000/dev/login/${acct.handle}`);
119 }
120 console.log(`${"=".repeat(60)}\n`);
121
122 // Handle shutdown
123 function shutdown() {
124 console.log("\nShutting down...");
125 for (const child of children) {
126 child.kill("SIGTERM");
127 }
128 process.exit(0);
129 }
130
131 process.on("SIGINT", shutdown);
132 process.on("SIGTERM", shutdown);
133
134 // Keep alive
135 await new Promise(() => {});
136}
137
138main().catch((err) => {
139 console.error("dev-full failed:", err);
140 for (const child of children) {
141 child.kill("SIGTERM");
142 }
143 process.exit(1);
144});