From 5d7f27e2963e0449416640962fe1fb96a9a11aa9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:48:54 -0700 Subject: [PATCH] nose-bbs --- .../use-bun-instead-of-node-vite-npm-pnpm.mdc | 1 + packages/nose-bbs/.gitignore | 34 +++++++ packages/nose-bbs/README.md | 6 ++ packages/nose-bbs/package.json | 20 ++++ packages/nose-bbs/src/server.tsx | 99 +++++++++++++++++++ packages/nose-bbs/tsconfig.json | 29 ++++++ 6 files changed, 189 insertions(+) create mode 120000 packages/nose-bbs/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc create mode 100644 packages/nose-bbs/.gitignore create mode 100644 packages/nose-bbs/README.md create mode 100644 packages/nose-bbs/package.json create mode 100644 packages/nose-bbs/src/server.tsx create mode 100644 packages/nose-bbs/tsconfig.json diff --git a/packages/nose-bbs/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/packages/nose-bbs/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 120000 index 0000000..6100270 --- /dev/null +++ b/packages/nose-bbs/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1 @@ +../../CLAUDE.md \ No newline at end of file diff --git a/packages/nose-bbs/.gitignore b/packages/nose-bbs/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/packages/nose-bbs/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/nose-bbs/README.md b/packages/nose-bbs/README.md new file mode 100644 index 0000000..da7c3df --- /dev/null +++ b/packages/nose-bbs/README.md @@ -0,0 +1,6 @@ +# nose-bbs + +## Quickstart + + bun install + bun dev \ No newline at end of file diff --git a/packages/nose-bbs/package.json b/packages/nose-bbs/package.json new file mode 100644 index 0000000..5faea3e --- /dev/null +++ b/packages/nose-bbs/package.json @@ -0,0 +1,20 @@ +{ + "name": "nose-bbs", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun subdomain:dev", + "subdomain:start": "bun run src/server.tsx", + "subdomain:dev": "bun run --hot src/server.tsx" + }, + "dependencies": { + "hono": "catalog:" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/nose-bbs/src/server.tsx b/packages/nose-bbs/src/server.tsx new file mode 100644 index 0000000..47368a7 --- /dev/null +++ b/packages/nose-bbs/src/server.tsx @@ -0,0 +1,99 @@ +import { Hono } from "hono" +import { Database } from "bun:sqlite" +import { join } from "path" + +export const DATA_DIR = process.env.DATA_DIR || "." +const api = new Hono() +const db = new Database(join(DATA_DIR, "bbs.sqlite")) + +api.use("*", async (c, next) => { + const method = c.req.method + const url = c.req.url + + let body = "" + if (method === "POST" || method === "PUT" || method === "PATCH") { + try { + const clonedRequest = c.req.raw.clone() + body = await clonedRequest.text() + console.log(`${method} ${url} - Body: ${body}`) + } catch (error) { + console.log(`${method} ${url} - Body: [unable to parse]`) + } + } else { + console.log(`${method} ${url}`) + } + + await next() +}) + +db.run(` + CREATE TABLE IF NOT EXISTS topics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS replies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + topic_id INTEGER NOT NULL, + content TEXT NOT NULL, + author TEXT NOT NULL, + created_at TEXT NOT NULL + ); +`) + +api.get("/topics", async c => { + const topics = db.query("SELECT * FROM topics ORDER BY id DESC").all() + return c.json(topics) +}) + +api.post("/topics", async c => { + const { title, content, author } = await c.req.json() + + if (!title || !content || !author) { + return c.json({ error: "title, content and author are required" }, 400) + } + + const topic = db.query(` + INSERT INTO + topics(title, content, author, created_at) + VALUES($title, $content, $author, $created_at) + `).run({ $title: title, $content: content, $author: author, $created_at: new Date().toISOString() }) + return c.json({ id: topic.lastInsertRowid, ok: true }) +}) + +api.get("/topics/:id", async c => { + const id = c.req.param("id") + if (!id) { + return c.json({ error: "id is required" }, 400) + } + + let topic = db.query("SELECT * FROM topics WHERE id = $id").get({ $id: id }) + if (!topic) { + return c.json({ error: "topic not found" }, 404) + } + const replies = db.query("SELECT * FROM replies WHERE topic_id = $id").all({ $id: id }) + ; (topic as any).replies = replies + + return c.json(topic) +}) + +api.post("/topics/:id/reply", async c => { + const id = c.req.param("id") + if (!id) { + return c.json({ error: "id is required" }, 400) + } + const { content, author } = await c.req.json() + const reply = db.query(` + INSERT INTO replies (topic_id, content, author, created_at) + VALUES ($topic_id, $content, $author, $created_at) + `).run({ $topic_id: id, $content: content, $author: author, $created_at: new Date().toISOString() }) + return c.json({ id: reply.lastInsertRowid, ok: true }) +}) + +export default { + port: process.env.PORT || 3001, + fetch: api.fetch, +} \ No newline at end of file diff --git a/packages/nose-bbs/tsconfig.json b/packages/nose-bbs/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/packages/nose-bbs/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}