From 609c79384409b42fb1f928588c4e8179fae140bd Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 11 Jul 2025 15:37:50 -0700 Subject: [PATCH] hmr with http --- packages/http/README.md | 5 +- packages/http/src/orchestrator.ts | 105 +++++++---------------- packages/http/src/routes/index.tsx | 2 +- packages/http/src/server.tsx | 12 ++- packages/nano-remix/src/buildRoute.ts | 16 +--- packages/project-whiteboard/package.json | 10 ++- packages/todo/package.json | 3 +- packages/werewolf-ui/package.json | 4 +- 8 files changed, 56 insertions(+), 101 deletions(-) diff --git a/packages/http/README.md b/packages/http/README.md index b710186..c94b823 100644 --- a/packages/http/README.md +++ b/packages/http/README.md @@ -5,5 +5,6 @@ A proxy server that will start all subdomain servers and a proxy server to route ## How to setup a subdomain server 1. Create a new package in the `packages` directory. -2. Add a `serve-subdomain` script to the `package.json` file of the new package. -3. It uses the directory name of the package to serve the subdomain. +2. Add a `subdomain:start` script to the `package.json` file of the new package. +3. Optionally add a `subdomain:dev` script for development (so you can add things like hot reloading). +4. It uses the directory name of the package to serve the subdomain. diff --git a/packages/http/src/orchestrator.ts b/packages/http/src/orchestrator.ts index cb5c37e..e1cd6db 100644 --- a/packages/http/src/orchestrator.ts +++ b/packages/http/src/orchestrator.ts @@ -3,72 +3,30 @@ import { basename, join } from "node:path" export const startSubdomainServers = async () => { const portMap: Record = {} - 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) - } + const packageInfo = await subdomainPackageInfo() + let currentPort = 3001 + + const isDevMode = process.env.NODE_ENV !== "production" + + const processes = packageInfo.map((info) => { + const port = currentPort++ + const dirname = basename(info.packagePath) + portMap[dirname] = port + + // Use subdomain:dev if available and in dev mode, otherwise use subdomain:start + const script = isDevMode && info.hasDevScript ? "subdomain:dev" : "subdomain:start" + + return run(["bun", "run", script], { + cwd: info.packagePath, + env: { PORT: port.toString() }, }) - } + }) - // 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() + Promise.all(processes).catch((err) => { + console.log(`โŒ Error starting subdomain servers:`, err) process.exit(1) }) - process.on("unhandledRejection", (reason) => { - console.error("โŒ Unhandled rejection:", reason) - cleanup() - process.exit(1) - }) - - try { - const packageInfo = await subdomainPackageInfo() - let currentPort = 3001 - - const processes = packageInfo.map((info) => { - const port = currentPort++ - portMap[info.dirName] = port - - 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) - } return portMap } @@ -76,7 +34,7 @@ export const startSubdomainServers = async () => { export const subdomainPackageInfo = async () => { const packagesDir = join(import.meta.dir, "../../") const packages = await readdir(packagesDir) - const packagePaths: { packageName: string; dirName: string }[] = [] + const packagePaths: { packageName: string; packagePath: string; hasDevScript: boolean }[] = [] for (const pkg of packages) { const packagePath = join(packagesDir, pkg) @@ -86,33 +44,28 @@ export const subdomainPackageInfo = async () => { const packageJson = await Bun.file(packageJsonPath).json() - if (packageJson.scripts?.["serve-subdomain"]) { - packagePaths.push({ packageName: packageJson.name, dirName: basename(pkg) }) + if (packageJson.scripts?.["subdomain:start"]) { + const hasDevScript = !!packageJson.scripts?.["subdomain:dev"] + packagePaths.push({ + packageName: basename(packagePath), + packagePath: packagePath, + hasDevScript, + }) } } return packagePaths } -const run = async ( - cmd: string[], - options: { env?: Record } = {}, - processTracker?: { proc: any; name: string }[], - processName?: string -) => { +const run = async (cmd: string[], options: { cwd: string; env?: Record }) => { const commandText = cmd.join(" ") const proc = Bun.spawn(cmd, { + cwd: options.cwd, stdout: "inherit", stderr: "inherit", 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) { diff --git a/packages/http/src/routes/index.tsx b/packages/http/src/routes/index.tsx index 7b860ce..ddbbc8c 100644 --- a/packages/http/src/routes/index.tsx +++ b/packages/http/src/routes/index.tsx @@ -15,7 +15,7 @@ export default function Index({ packagePaths }: LoaderProps) { diff --git a/packages/http/src/server.tsx b/packages/http/src/server.tsx index de6ed4a..554c01f 100644 --- a/packages/http/src/server.tsx +++ b/packages/http/src/server.tsx @@ -10,15 +10,21 @@ import { join } from "node:path" * 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 (*.thedomain.com) + * 5. In development, uses subdomain:dev scripts if available for hot reloading * * This server is designed to be run in production with NODE_ENV=production. On development, * it allows unauthenticated access because YOLO. */ const serve = async () => { - const portMap = await startSubdomainServers() - const server = startServer(portMap) - logServerInfo(server, portMap) + try { + const portMap = await startSubdomainServers() + const server = startServer(portMap) + logServerInfo(server, portMap) + } catch (error) { + console.error("โŒ Error starting http package:", error) + process.exit(1) + } } serve() diff --git a/packages/nano-remix/src/buildRoute.ts b/packages/nano-remix/src/buildRoute.ts index 0ff12cd..8c9c09b 100644 --- a/packages/nano-remix/src/buildRoute.ts +++ b/packages/nano-remix/src/buildRoute.ts @@ -1,4 +1,4 @@ -import { join } from "node:path" +import { join, dirname } from "node:path" type BuildRouteOptions = { distDir: string @@ -8,7 +8,7 @@ type BuildRouteOptions = { } export const buildRoute = async ({ distDir, routeName, filepath, force = false }: BuildRouteOptions) => { - if (!force && !(await shouldRebuild(routeName, filepath, distDir))) { + if (!force && !(await needsBuild(routeName, distDir))) { return } @@ -31,7 +31,7 @@ export const buildRoute = async ({ distDir, routeName, filepath, force = false } console.log(stdout) } -const shouldRebuild = async (routeName: string, sourceFilePath: string, distDir: string) => { +const needsBuild = async (routeName: string, distDir: string) => { if (process.env.NODE_ENV !== "production") { return true } @@ -39,15 +39,7 @@ const shouldRebuild = async (routeName: string, sourceFilePath: string, distDir: try { const outputPath = join(distDir, routeName + ".js") - // If output doesn't exist, need to build - if (!(await Bun.file(outputPath).exists())) { - return true - } - - const sourceModified = await Bun.file(sourceFilePath).lastModified - const outputModified = await Bun.file(outputPath).lastModified - - return sourceModified > outputModified + return !(await Bun.file(outputPath).exists()) } catch (error) { return true } diff --git a/packages/project-whiteboard/package.json b/packages/project-whiteboard/package.json index d8448fa..4d7387c 100644 --- a/packages/project-whiteboard/package.json +++ b/packages/project-whiteboard/package.json @@ -1,8 +1,13 @@ { - "name": "@workspace/project-whiteboard", + "name": "@workshop/project-whiteboard", "module": "index.ts", "type": "module", "private": true, + "scripts": { + "dev": "bun run --hot src/server.ts", + "subdomain:start": "bun run src/server.ts", + "subdomain:dev": "bun run --hot src/server.ts" + }, "dependencies": { "@workshop/shared": "workspace:*", "@workshop/nano-remix": "workspace:*", @@ -13,8 +18,5 @@ }, "peerDependencies": { "typescript": "^5" - }, - "scripts": { - "serve-subdomain": "bun run src/server.ts" } } \ No newline at end of file diff --git a/packages/todo/package.json b/packages/todo/package.json index a0abd80..8b2c7d0 100644 --- a/packages/todo/package.json +++ b/packages/todo/package.json @@ -6,7 +6,8 @@ "private": true, "scripts": { "dev": "bun run src/server.tsx", - "serve-subdomain": "bun run src/server.tsx" + "subdomain:start": "bun run src/server.tsx", + "subdomain:dev": "bun run --hot src/server.tsx" }, "dependencies": { "@workshop/nano-remix": "workspace:*", diff --git a/packages/werewolf-ui/package.json b/packages/werewolf-ui/package.json index ffb9f97..87ebda1 100644 --- a/packages/werewolf-ui/package.json +++ b/packages/werewolf-ui/package.json @@ -6,8 +6,8 @@ "main": "src/index.tsx", "module": "src/index.tsx", "scripts": { - "dev": "bun --hot src/server.tsx", - "serve-subdomain": "bun src/server.tsx" + "subdomain:start": "bun src/server.tsx", + "subdomain:dev": "bun run --hot src/server.tsx " }, "prettier": { "semi": false,