Add project-whiteboard app and update dependencies

This commit is contained in:
Corey Johnson 2025-07-11 10:50:13 -07:00
parent b758f66c69
commit fc9288d0cc
18 changed files with 367 additions and 19 deletions

109
.github/instructions/bun.instructions.md vendored Normal file
View File

@ -0,0 +1,109 @@
---
applyTo: "**/*.{js,ts,jsx,tsx}"
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

@ -8,7 +8,7 @@
"name": "attache",
"dependencies": {
"@workshop/shared": "workspace:*",
"hono": "^4.8.0",
"hono": "catalog:",
"nanoid": "^5.1.5",
},
"devDependencies": {
@ -59,6 +59,20 @@
"typescript": "^5",
},
},
"packages/project-whitespace": {
"name": "@workspace/project-whitespace",
"dependencies": {
"@workshop/nano-remix": "workspace:*",
"@workshop/shared": "workspace:*",
"hono": "catalog:",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
"packages/query": {
"name": "@workshop/query",
"dependencies": {
@ -109,6 +123,7 @@
"@lezer/generator": "^1.7.3",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@workshop/nano-remix": "workspace:*",
"hono": "catalog:",
"luxon": "^3.6.1",
"zzfx": "^1.3.0",
@ -117,7 +132,6 @@
"@types/bun": "latest",
},
"peerDependencies": {
"hono": "^4",
"typescript": "^5",
},
},
@ -125,7 +139,8 @@
"name": "@workshop/werewolf-ui",
"version": "0.1.0",
"dependencies": {
"hono": "^4.8.3",
"@workshop/nano-remix": "workspace:*",
"hono": "catalog:",
"lucide-static": "^0.525.0",
"tailwindcss": "^4.0.6",
},
@ -222,6 +237,8 @@
"@workshop/werewolf-ui": ["@workshop/werewolf-ui@workspace:packages/werewolf-ui"],
"@workspace/project-whitespace": ["@workspace/project-whitespace@workspace:packages/project-whitespace"],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
@ -782,6 +799,8 @@
"@workshop/nano-remix/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"@workshop/shared/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@workshop/todo/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@workshop/todo/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
@ -790,12 +809,20 @@
"@workshop/werewolf-ui/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"@workspace/project-whitespace/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@workspace/project-whitespace/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"ajv/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
"amqplib/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"attache/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"attache/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
"body-parser/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@ -896,14 +923,20 @@
"@workshop/nano-remix/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workshop/shared/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workshop/todo/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workshop/werewolf-ui/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workspace/project-whitespace/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"amqplib/readable-stream/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
"attache/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],

View File

@ -24,7 +24,13 @@ const run = async (cmd: string[]) => {
}
try {
await Promise.all([run(["bun", "run", "--filter=@workshop/http", "start"]), run(["bun", "bot:discord"])])
const isDev = process.env.NODE_ENV !== "production"
const noElide = isDev ? "--elide-lines=0" : ""
await Promise.all([
run(["bun", "run", noElide, "--filter=@workshop/http", "start"]),
run(["bun", "run", noElide, "bot:discord"]),
])
console.log("✅ All processes completed successfully")
} catch (error) {
console.error("❌ One or more processes failed:", error)

View File

@ -14,8 +14,8 @@
"typescript": "^5"
},
"dependencies": {
"hono": "^4.8.0",
"hono": "catalog:",
"nanoid": "^5.1.5",
"@workshop/shared": "workspace:*"
}
}
}

View File

@ -3,6 +3,43 @@ import { basename, join } from "node:path"
export const startSubdomainServers = async () => {
const portMap: Record<string, number> = {}
const spawnedProcesses: { proc: any; name: string }[] = []
// Cleanup function to kill all spawned processes
const cleanup = () => {
console.log("🧹 Cleaning up spawned processes...")
spawnedProcesses.forEach(({ proc, name }) => {
try {
if (!proc.killed) {
console.log(`🔪 Killing process: ${name} (PID: ${proc.pid})`)
proc.kill("SIGTERM") // Just send SIGTERM and let the OS handle the rest
}
} catch (err) {
console.warn(`⚠️ Error killing process ${name}:`, err)
}
})
}
// Register cleanup handlers
process.on("exit", cleanup)
process.on("SIGINT", () => {
cleanup()
process.exit(0)
})
process.on("SIGTERM", () => {
cleanup()
process.exit(0)
})
process.on("uncaughtException", (err) => {
console.error("❌ Uncaught exception:", err)
cleanup()
process.exit(1)
})
process.on("unhandledRejection", (reason) => {
console.error("❌ Unhandled rejection:", reason)
cleanup()
process.exit(1)
})
try {
const packageInfo = await subdomainPackageInfo()
@ -12,17 +49,24 @@ export const startSubdomainServers = async () => {
const port = currentPort++
portMap[info.dirName] = port
return run(["bun", "run", `--filter=${info.packageName}`, "serve-subdomain"], {
env: { PORT: port.toString() },
})
return run(
["bun", "run", `--filter=${info.packageName}`, "serve-subdomain"],
{
env: { PORT: port.toString() },
},
spawnedProcesses,
info.packageName
)
})
Promise.all(processes).catch((err) => {
console.log(`❌ Error starting subdomain servers:`, err)
cleanup()
process.exit(1)
})
} catch (error) {
console.error("❌ Error starting subdomain servers:", error)
cleanup()
process.exit(1)
}
@ -50,7 +94,12 @@ export const subdomainPackageInfo = async () => {
return packagePaths
}
const run = async (cmd: string[], options: { env?: Record<string, string> } = {}) => {
const run = async (
cmd: string[],
options: { env?: Record<string, string> } = {},
processTracker?: { proc: any; name: string }[],
processName?: string
) => {
const commandText = cmd.join(" ")
const proc = Bun.spawn(cmd, {
stdout: "inherit",
@ -58,6 +107,12 @@ const run = async (cmd: string[], options: { env?: Record<string, string> } = {}
env: { ...process.env, ...options.env },
})
// Track the process if tracker is provided
if (processTracker && processName) {
processTracker.push({ proc, name: processName })
console.log(`🚀 Started process: ${processName} (PID: ${proc.pid})`)
}
const status = await proc.exited
if (status !== 0) {

View File

@ -9,7 +9,7 @@ import { join } from "node:path"
* 1. Starts multiple subdomain servers on different ports (3001+)
* 2. Main server (port 3000) acts as a proxy that routes requests based on subdomain
* 3. All requests are protected with Basic HTTP auth + persistent cookies
* 4. Once authenticated, cookies work across all subdomains (*.yourdomain.com)
* 4. Once authenticated, cookies work across all subdomains (*.thedomain.com)
*
* This server is designed to be run in production with NODE_ENV=production. On development,
* it allows unauthenticated access because YOLO.

View File

@ -5,7 +5,7 @@ import { join, extname } from "node:path"
type Options = {
routesDir?: string
distDir?: string
disableCache?: boolean // Disable caching for development
disableCache?: boolean
}
export const nanoRemix = async (req: Request, options: Options = {}) => {
const nanoRemixDir = join(process.cwd(), ".nano-remix")

34
packages/project-whiteboard/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,15 @@
# project-whitespace
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@ -0,0 +1,20 @@
{
"name": "@workspace/project-whiteboard",
"module": "index.ts",
"type": "module",
"private": true,
"dependencies": {
"@workshop/shared": "workspace:*",
"@workshop/nano-remix": "workspace:*",
"hono": "catalog:"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"scripts": {
"serve-subdomain": "bun run src/server.ts"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@ -0,0 +1,8 @@
export default function Index() {
return (
<div>
<h1>PROJECT WHITEBOARD</h1>
<img src="/whiteboard.jpeg" alt="Project Whiteboard" />
</div>
)
}

View File

@ -0,0 +1,15 @@
import { nanoRemix } from "@workshop/nano-remix"
import { join } from "path"
Bun.serve({
routes: {
"/": {
GET: (req) => {
return nanoRemix(req, {
routePath: join(import.meta.dir, "routes"),
publicDir: join(import.meta.dir, "..", "public"),
})
},
},
},
})

View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/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,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -9,6 +9,7 @@
"serve-subdomain": "bun run src/server.tsx"
},
"dependencies": {
"@workshop/nano-remix": "workspace:*",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/language": "^6.11.1",
@ -30,7 +31,6 @@
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5",
"hono": "^4"
"typescript": "^5"
}
}

View File

@ -14,7 +14,8 @@
"printWidth": 110
},
"dependencies": {
"hono": "^4.8.3",
"@workshop/nano-remix": "workspace:*",
"hono": "catalog:",
"lucide-static": "^0.525.0",
"tailwindcss": "^4.0.6"
},

View File

@ -1,18 +1,35 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["dist", "node_modules"]
}
}