From f4df41a0d76a5d39cda7b93a09d2991d934aa782 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Feb 2026 21:36:07 -0800 Subject: [PATCH] Add dev runner for deterministic multi-instance port offsets - Route `dev*` scripts through `scripts/dev-runner.mjs` to compute shared server/web port offsets - Support `T3CODE_DEV_INSTANCE` (hashed or numeric) and explicit `T3CODE_PORT_OFFSET` - Document multi-instance usage and allow server `VITE_DEV_SERVER_URL` override --- README.md | 13 ++++ apps/server/package.json | 2 +- package.json | 8 +-- scripts/dev-runner.mjs | 124 +++++++++++++++++++++++++++++++++++++++ turbo.json | 1 + 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 scripts/dev-runner.mjs diff --git a/README.md b/README.md index 93f955361e0..ac73a427082 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ bun run dev # Desktop development bun run dev:desktop +# Desktop development on an isolated port set +T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop + # Production bun run build bun run start @@ -67,6 +70,16 @@ npx t3 - `bun run typecheck` — Strict TypeScript checks for all packages. - `bun run test` — Runs workspace tests. +### Running multiple dev instances + +Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together. + +- Default ports: server `3773`, web `5173` +- Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`) +- Example: `T3CODE_DEV_INSTANCE=branch-a bun run dev:desktop` + +If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset. + ## Runtime modes T3 Code has a global runtime mode switch in the chat toolbar: diff --git a/apps/server/package.json b/apps/server/package.json index 46debc0e5cd..69ac42c4809 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -10,7 +10,7 @@ "type": "module", "main": "./dist/index.mjs", "scripts": { - "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 bun run src/index.ts", + "dev": "VITE_DEV_SERVER_URL=${VITE_DEV_SERVER_URL:-http://localhost:5173} bun run src/index.ts", "build": "tsdown && node scripts/bundle-client.mjs", "start": "node dist/index.mjs", "typecheck": "tsc --noEmit", diff --git a/package.json b/package.json index 106d67ba230..e15f074db43 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "packages/*" ], "scripts": { - "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3 --parallel", - "dev:server": "turbo run dev --filter=t3", - "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo run dev --filter=@t3tools/web", - "dev:desktop": "turbo run dev --filter=@t3tools/desktop --filter=@t3tools/web --parallel", + "dev": "bun run scripts/dev-runner.mjs dev", + "dev:server": "bun run scripts/dev-runner.mjs dev:server", + "dev:web": "bun run scripts/dev-runner.mjs dev:web", + "dev:desktop": "bun run scripts/dev-runner.mjs dev:desktop", "start": "turbo run start --filter=t3", "start:desktop": "turbo run start --filter=@t3tools/desktop", "build": "turbo run build", diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs new file mode 100644 index 00000000000..89b9ef3c26c --- /dev/null +++ b/scripts/dev-runner.mjs @@ -0,0 +1,124 @@ +import { spawn } from "node:child_process"; + +const BASE_SERVER_PORT = 3773; +const BASE_WEB_PORT = 5173; +const MAX_HASH_OFFSET = 3000; +const MODE_ARGS = { + dev: [ + "run", + "dev", + "--ui=tui", + "--filter=@t3tools/contracts", + "--filter=@t3tools/web", + "--filter=t3", + "--parallel", + ], + "dev:server": ["run", "dev", "--filter=t3"], + "dev:web": ["run", "dev", "--filter=@t3tools/web"], + "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], +}; + +function parseInteger(value, envName) { + const parsed = Number(value); + if (!Number.isInteger(parsed)) { + throw new Error(`Invalid ${envName}: ${value}`); + } + return parsed; +} + +function hashSeed(seed) { + let hash = 0x811c9dc5; + for (let i = 0; i < seed.length; i += 1) { + hash ^= seed.charCodeAt(i); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} + +function resolveOffset() { + const explicitOffset = process.env.T3CODE_PORT_OFFSET?.trim(); + if (explicitOffset) { + const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); + if (parsed < 0) { + throw new Error(`Invalid T3CODE_PORT_OFFSET: ${explicitOffset}`); + } + return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` }; + } + + const seed = process.env.T3CODE_DEV_INSTANCE?.trim(); + if (!seed) { + return { offset: 0, source: "default ports" }; + } + + if (/^\d+$/.test(seed)) { + return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` }; + } + + const offset = (hashSeed(seed) % MAX_HASH_OFFSET) + 1; + return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; +} + +function main() { + const mode = process.argv[2]; + const isDryRun = process.argv.includes("--dry-run"); + if (!mode || !(mode in MODE_ARGS)) { + const supportedModes = Object.keys(MODE_ARGS).join(", "); + throw new Error(`Usage: bun scripts/dev-runner.mjs . Supported modes: ${supportedModes}`); + } + + const { offset, source } = resolveOffset(); + const serverPort = BASE_SERVER_PORT + offset; + const webPort = BASE_WEB_PORT + offset; + + if (serverPort > 65535 || webPort > 65535) { + throw new Error( + `Port offset too large (${offset}). Computed ports: server=${serverPort}, web=${webPort}`, + ); + } + + const env = { + ...process.env, + T3CODE_PORT: String(serverPort), + PORT: String(webPort), + ELECTRON_RENDERER_PORT: String(webPort), + VITE_WS_URL: `ws://localhost:${serverPort}`, + VITE_DEV_SERVER_URL: `http://localhost:${webPort}`, + }; + if (mode === "dev" && !env.T3CODE_LOG_WS_EVENTS) { + env.T3CODE_LOG_WS_EVENTS = "1"; + } + + console.info( + `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort}`, + ); + + if (isDryRun) { + return; + } + + const command = process.platform === "win32" ? "turbo.cmd" : "turbo"; + const child = spawn(command, MODE_ARGS[mode], { + stdio: "inherit", + env, + }); + + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); + }); + + child.on("error", (error) => { + console.error("[dev-runner] failed to start turbo", error); + process.exit(1); + }); +} + +try { + main(); +} catch (error) { + console.error("[dev-runner]", error); + process.exit(1); +} diff --git a/turbo.json b/turbo.json index 6d0970bf95c..4ad892445af 100644 --- a/turbo.json +++ b/turbo.json @@ -4,6 +4,7 @@ "PORT", "VITE_WS_URL", "VITE_DEV_SERVER_URL", + "ELECTRON_RENDERER_PORT", "T3CODE_LOG_WS_EVENTS", "T3CODE_MODE", "T3CODE_PORT",