hmr with http

This commit is contained in:
Corey Johnson 2025-07-11 15:37:50 -07:00
parent d4c725465a
commit 609c793844
8 changed files with 56 additions and 101 deletions

View File

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

View File

@ -3,72 +3,30 @@ 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)
}
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<string, string> } = {},
processTracker?: { proc: any; name: string }[],
processName?: string
) => {
const run = async (cmd: string[], options: { cwd: string; env?: Record<string, string> }) => {
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) {

View File

@ -15,7 +15,7 @@ export default function Index({ packagePaths }: LoaderProps<typeof loader>) {
<ul>
{packagePaths.map((pkg) => (
<li key={pkg.packageName}>
<a href={`${url.protocol}//${pkg.dirName}.${url.host}`}>{pkg.packageName}</a>
<a href={`${url.protocol}//${pkg.packageName}.${url.host}`}>{pkg.packageName}</a>
</li>
))}
</ul>

View File

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

View File

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

View File

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

View File

@ -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:*",

View File

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