hmr with http
This commit is contained in:
parent
d4c725465a
commit
609c793844
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user