| ... | ... | @@ -1,8 +1,12 @@ |
| 1 | 1 | import { spawn } from "node:child_process"; |
| 2 | import { homedir } from "node:os"; |
| 3 | import path from "node:path"; |
| 4 | import { pathToFileURL } from "node:url"; |
| 2 | 5 | |
| 3 | 6 | const BASE_SERVER_PORT = 3773; |
| 4 | 7 | const BASE_WEB_PORT = 5173; |
| 5 | 8 | const MAX_HASH_OFFSET = 3000; |
| 9 | export const DEFAULT_DEV_STATE_DIR = path.join(homedir(), ".t3", "dev"); |
| 6 | 10 | const MODE_ARGS = { |
| 7 | 11 | dev: [ |
| 8 | 12 | "run", |
| ... | ... | @@ -17,6 +21,21 @@ const MODE_ARGS = { |
| 17 | 21 | "dev:web": ["run", "dev", "--filter=@t3tools/web"], |
| 18 | 22 | "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], |
| 19 | 23 | }; |
| 24 | const FORWARDED_ENV_FLAGS = { |
| 25 | "state-dir": { envName: "T3CODE_STATE_DIR", expectsValue: true }, |
| 26 | "auth-token": { envName: "T3CODE_AUTH_TOKEN", expectsValue: true }, |
| 27 | token: { envName: "T3CODE_AUTH_TOKEN", expectsValue: true }, |
| 28 | "no-browser": { envName: "T3CODE_NO_BROWSER", expectsValue: false }, |
| 29 | "auto-bootstrap-project-from-cwd": { |
| 30 | envName: "T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD", |
| 31 | expectsValue: false, |
| 32 | }, |
| 33 | "log-websocket-events": { envName: "T3CODE_LOG_WS_EVENTS", expectsValue: false }, |
| 34 | "log-ws-events": { envName: "T3CODE_LOG_WS_EVENTS", expectsValue: false }, |
| 35 | host: { envName: "T3CODE_HOST", expectsValue: true }, |
| 36 | port: { envName: "T3CODE_PORT", expectsValue: true }, |
| 37 | "dev-url": { envName: "VITE_DEV_SERVER_URL", expectsValue: true }, |
| 38 | }; |
| 20 | 39 | |
| 21 | 40 | function parseInteger(value, envName) { |
| 22 | 41 | const parsed = Number(value); |
| ... | ... | @@ -35,8 +54,8 @@ function hashSeed(seed) { |
| 35 | 54 | return hash >>> 0; |
| 36 | 55 | } |
| 37 | 56 | |
| 38 | | function resolveOffset() { |
| 39 | | const explicitOffset = process.env.T3CODE_PORT_OFFSET?.trim(); |
| 57 | export function resolveOffset(env = process.env) { |
| 58 | const explicitOffset = env.T3CODE_PORT_OFFSET?.trim(); |
| 40 | 59 | if (explicitOffset) { |
| 41 | 60 | const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); |
| 42 | 61 | if (parsed < 0) { |
| ... | ... | @@ -45,7 +64,7 @@ function resolveOffset() { |
| 45 | 64 | return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` }; |
| 46 | 65 | } |
| 47 | 66 | |
| 48 | | const seed = process.env.T3CODE_DEV_INSTANCE?.trim(); |
| 67 | const seed = env.T3CODE_DEV_INSTANCE?.trim(); |
| 49 | 68 | if (!seed) { |
| 50 | 69 | return { offset: 0, source: "default ports" }; |
| 51 | 70 | } |
| ... | ... | @@ -58,55 +77,159 @@ function resolveOffset() { |
| 58 | 77 | return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; |
| 59 | 78 | } |
| 60 | 79 | |
| 61 | | function main() { |
| 62 | | const mode = process.argv[2]; |
| 63 | | const isDryRun = process.argv.includes("--dry-run"); |
| 64 | | if (!mode || !(mode in MODE_ARGS)) { |
| 65 | | const supportedModes = Object.keys(MODE_ARGS).join(", "); |
| 66 | | throw new Error(`Usage: bun scripts/dev-runner.mjs <mode>. Supported modes: ${supportedModes}`); |
| 80 | function parseLongFlag(raw) { |
| 81 | if (!raw.startsWith("--")) return null; |
| 82 | const normalized = raw.slice(2); |
| 83 | const equalsIndex = normalized.indexOf("="); |
| 84 | if (equalsIndex === -1) { |
| 85 | return { name: normalized, value: undefined }; |
| 67 | 86 | } |
| 87 | return { |
| 88 | name: normalized.slice(0, equalsIndex), |
| 89 | value: normalized.slice(equalsIndex + 1), |
| 90 | }; |
| 91 | } |
| 68 | 92 | |
| 69 | | const { offset, source } = resolveOffset(); |
| 70 | | const serverPort = BASE_SERVER_PORT + offset; |
| 71 | | const webPort = BASE_WEB_PORT + offset; |
| 93 | function toBooleanEnvValue(value) { |
| 94 | if (value === undefined) return "1"; |
| 95 | const normalized = value.trim().toLowerCase(); |
| 96 | if (normalized === "false" || normalized === "0" || normalized === "no") return "0"; |
| 97 | return "1"; |
| 98 | } |
| 72 | 99 | |
| 73 | | if (serverPort > 65535 || webPort > 65535) { |
| 74 | | throw new Error( |
| 75 | | `Port offset too large (${offset}). Computed ports: server=${serverPort}, web=${webPort}`, |
| 76 | | ); |
| 100 | export function parseDevRunnerArgs(rawArgs) { |
| 101 | const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs.slice(); |
| 102 | const envOverrides = {}; |
| 103 | const turboArgs = []; |
| 104 | let isDryRun = false; |
| 105 | |
| 106 | for (let index = 0; index < args.length; index += 1) { |
| 107 | const arg = args[index]; |
| 108 | if (arg === "--dry-run") { |
| 109 | isDryRun = true; |
| 110 | continue; |
| 111 | } |
| 112 | |
| 113 | if (arg === "--") { |
| 114 | turboArgs.push(...args.slice(index + 1)); |
| 115 | break; |
| 116 | } |
| 117 | |
| 118 | const parsedFlag = parseLongFlag(arg); |
| 119 | if (!parsedFlag) { |
| 120 | turboArgs.push(arg); |
| 121 | continue; |
| 122 | } |
| 123 | |
| 124 | const definition = FORWARDED_ENV_FLAGS[parsedFlag.name]; |
| 125 | if (!definition) { |
| 126 | turboArgs.push(arg); |
| 127 | continue; |
| 128 | } |
| 129 | |
| 130 | if (definition.expectsValue) { |
| 131 | let value = parsedFlag.value; |
| 132 | if (value === undefined) { |
| 133 | const next = args[index + 1]; |
| 134 | if (next === undefined) { |
| 135 | throw new Error(`Missing value for --${parsedFlag.name}`); |
| 136 | } |
| 137 | value = next; |
| 138 | index += 1; |
| 139 | } |
| 140 | envOverrides[definition.envName] = value; |
| 141 | continue; |
| 142 | } |
| 143 | |
| 144 | envOverrides[definition.envName] = toBooleanEnvValue(parsedFlag.value); |
| 77 | 145 | } |
| 78 | 146 | |
| 79 | | const env = { |
| 80 | | ...process.env, |
| 147 | return { |
| 148 | isDryRun, |
| 149 | envOverrides, |
| 150 | turboArgs, |
| 151 | }; |
| 152 | } |
| 153 | |
| 154 | function resolveStateDir(envOverrides) { |
| 155 | return envOverrides.T3CODE_STATE_DIR ?? DEFAULT_DEV_STATE_DIR; |
| 156 | } |
| 157 | |
| 158 | export function createDevRunnerEnv({ mode, env, offset, envOverrides }) { |
| 159 | const serverPort = BASE_SERVER_PORT + offset; |
| 160 | const webPort = BASE_WEB_PORT + offset; |
| 161 | |
| 162 | const output = { |
| 163 | ...env, |
| 81 | 164 | T3CODE_PORT: String(serverPort), |
| 82 | 165 | PORT: String(webPort), |
| 83 | 166 | ELECTRON_RENDERER_PORT: String(webPort), |
| 84 | 167 | VITE_WS_URL: `ws://localhost:${serverPort}`, |
| 85 | 168 | VITE_DEV_SERVER_URL: `http://localhost:${webPort}`, |
| 169 | ...envOverrides, |
| 86 | 170 | }; |
| 87 | 171 | |
| 172 | output.T3CODE_STATE_DIR = resolveStateDir(envOverrides); |
| 173 | |
| 174 | const parsedServerPort = Number(output.T3CODE_PORT); |
| 175 | if (Number.isInteger(parsedServerPort) && parsedServerPort > 0 && parsedServerPort <= 65535) { |
| 176 | output.VITE_WS_URL = `ws://localhost:${parsedServerPort}`; |
| 177 | } |
| 178 | |
| 88 | 179 | if (mode === "dev" || mode === "dev:server" || mode === "dev:web") { |
| 89 | 180 | // Running server/web in browser mode should not inherit desktop launcher state. |
| 90 | | env.T3CODE_MODE = "web"; |
| 91 | | delete env.T3CODE_NO_BROWSER; |
| 92 | | delete env.T3CODE_AUTH_TOKEN; |
| 93 | | delete env.T3CODE_DESKTOP_WS_URL; |
| 181 | output.T3CODE_MODE = "web"; |
| 182 | if (!("T3CODE_NO_BROWSER" in envOverrides)) { |
| 183 | delete output.T3CODE_NO_BROWSER; |
| 184 | } |
| 185 | if (!("T3CODE_AUTH_TOKEN" in envOverrides)) { |
| 186 | delete output.T3CODE_AUTH_TOKEN; |
| 187 | } |
| 188 | delete output.T3CODE_DESKTOP_WS_URL; |
| 189 | } |
| 190 | |
| 191 | if (mode === "dev" && !output.T3CODE_LOG_WS_EVENTS) { |
| 192 | output.T3CODE_LOG_WS_EVENTS = "1"; |
| 193 | } |
| 194 | |
| 195 | return output; |
| 196 | } |
| 197 | |
| 198 | export function runDevRunner(argv = process.argv) { |
| 199 | const mode = argv[2]; |
| 200 | const parsedArgs = parseDevRunnerArgs(argv.slice(3)); |
| 201 | if (!mode || !(mode in MODE_ARGS)) { |
| 202 | const supportedModes = Object.keys(MODE_ARGS).join(", "); |
| 203 | throw new Error(`Usage: bun scripts/dev-runner.mjs <mode>. Supported modes: ${supportedModes}`); |
| 94 | 204 | } |
| 95 | 205 | |
| 96 | | if (mode === "dev" && !env.T3CODE_LOG_WS_EVENTS) { |
| 97 | | env.T3CODE_LOG_WS_EVENTS = "1"; |
| 206 | const { offset, source } = resolveOffset(); |
| 207 | const serverPort = BASE_SERVER_PORT + offset; |
| 208 | const webPort = BASE_WEB_PORT + offset; |
| 209 | |
| 210 | if (serverPort > 65535 || webPort > 65535) { |
| 211 | throw new Error( |
| 212 | `Port offset too large (${offset}). Computed ports: server=${serverPort}, web=${webPort}`, |
| 213 | ); |
| 98 | 214 | } |
| 99 | 215 | |
| 216 | const env = createDevRunnerEnv({ |
| 217 | mode, |
| 218 | env: process.env, |
| 219 | offset, |
| 220 | envOverrides: parsedArgs.envOverrides, |
| 221 | }); |
| 222 | |
| 100 | 223 | console.info( |
| 101 | | `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort}`, |
| 224 | `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort} stateDir=${env.T3CODE_STATE_DIR}`, |
| 102 | 225 | ); |
| 103 | 226 | |
| 104 | | if (isDryRun) { |
| 227 | if (parsedArgs.isDryRun) { |
| 105 | 228 | return; |
| 106 | 229 | } |
| 107 | 230 | |
| 108 | 231 | const command = process.platform === "win32" ? "turbo.cmd" : "turbo"; |
| 109 | | const child = spawn(command, MODE_ARGS[mode], { |
| 232 | const child = spawn(command, [...MODE_ARGS[mode], ...parsedArgs.turboArgs], { |
| 110 | 233 | stdio: "inherit", |
| 111 | 234 | env, |
| 112 | 235 | }); |
| ... | ... | @@ -125,9 +248,13 @@ function main() { |
| 125 | 248 | }); |
| 126 | 249 | } |
| 127 | 250 | |
| 128 | | try { |
| 129 | | main(); |
| 130 | | } catch (error) { |
| 131 | | console.error("[dev-runner]", error); |
| 132 | | process.exit(1); |
| 251 | const entrypointPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : ""; |
| 252 | |
| 253 | if (import.meta.url === entrypointPath) { |
| 254 | try { |
| 255 | runDevRunner(); |
| 256 | } catch (error) { |
| 257 | console.error("[dev-runner]", error); |
| 258 | process.exit(1); |
| 259 | } |
| 133 | 260 | } |