Skip to content

Commit bbdc614

Browse files
committed
Fix dev server port conflicts across worktrees
Running multiple worktrees in parallel (Conductor or plain git worktree) caused studios on 5174/5175 to silently proxy to the first worktree's API on :3001, serving stale code from the wrong branch. A root orchestrator (scripts/dev.mjs) now picks a free port and threads PORT + API_PROXY_TARGET into the api/studio children via env vars. The vite proxy reads API_PROXY_TARGET with a :3001 fallback so solo dev and prod builds are unaffected. Port-probe helper adapted from the findFreePort in zig/electron.
1 parent b5352b0 commit bbdc614

File tree

4 files changed

+62
-2
lines changed

4 files changed

+62
-2
lines changed

apps/studio/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default defineConfig({
2626
open: true,
2727
proxy: {
2828
"/api": {
29-
target: "http://localhost:3001",
29+
target: process.env.API_PROXY_TARGET ?? "http://localhost:3001",
3030
changeOrigin: true,
3131
},
3232
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"typecheck": "tsc --build",
1313
"lint": "eslint",
1414
"build": "tsc --build",
15-
"dev": "pnpm --parallel --filter @adt/api --filter @adt/studio dev",
15+
"dev": "node scripts/dev.mjs",
1616
"dev:api": "pnpm --filter @adt/api dev",
1717
"dev:studio": "pnpm --filter @adt/studio dev",
1818
"dev:desktop": "pnpm --filter @adt/desktop dev",

scripts/dev.mjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { spawn } from "node:child_process"
2+
import { findFreePort } from "./find-free-port.mjs"
3+
4+
const apiPort = await findFreePort({ start: 3001 })
5+
if (!apiPort) {
6+
console.error("[dev] could not find a free port for the API in 3001-49151")
7+
process.exit(1)
8+
}
9+
10+
const proxyTarget = `http://localhost:${apiPort}`
11+
console.log(`[dev] API port: ${apiPort}`)
12+
console.log(`[dev] Studio proxy target: ${proxyTarget}`)
13+
14+
const child = spawn(
15+
"pnpm",
16+
["--parallel", "--filter", "@adt/api", "--filter", "@adt/studio", "dev"],
17+
{
18+
stdio: "inherit",
19+
shell: process.platform === "win32",
20+
env: {
21+
...process.env,
22+
PORT: String(apiPort),
23+
API_PROXY_TARGET: proxyTarget,
24+
},
25+
},
26+
)
27+
28+
const forward = (signal) => () => {
29+
if (!child.killed) child.kill(signal)
30+
}
31+
process.on("SIGINT", forward("SIGINT"))
32+
process.on("SIGTERM", forward("SIGTERM"))
33+
34+
child.on("exit", (code, signal) => {
35+
if (signal) process.kill(process.pid, signal)
36+
else process.exit(code ?? 0)
37+
})

scripts/find-free-port.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createServer } from "node:net"
2+
3+
function canListenOnPort(port) {
4+
return new Promise((resolve) => {
5+
const server = createServer()
6+
server.once("error", () => resolve(false))
7+
server.once("listening", () => server.close(() => resolve(true)))
8+
server.listen(port)
9+
})
10+
}
11+
12+
export async function findFreePort({ start = 3001, end = 49151, maxAttempts = 200 } = {}) {
13+
const limit = Math.min(maxAttempts, end - start + 1)
14+
for (let i = 0; i < limit; i++) {
15+
const port = start + i
16+
if (await canListenOnPort(port)) return port
17+
}
18+
return undefined
19+
}
20+
21+
export async function portTaken(port) {
22+
return !(await canListenOnPort(port))
23+
}

0 commit comments

Comments
 (0)