| ... | @@ -1,8 +1,12 @@ | ... | @@ -1,8 +1,12 @@ |
| 1 | import { spawn } from "node:child_process"; | 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 | const BASE_SERVER_PORT = 3773; | 6 | const BASE_SERVER_PORT = 3773; |
| 4 | const BASE_WEB_PORT = 5173; | 7 | const BASE_WEB_PORT = 5173; |
| 5 | const MAX_HASH_OFFSET = 3000; | 8 | const MAX_HASH_OFFSET = 3000; |
| | 9 | export const DEFAULT_DEV_STATE_DIR = path.join(homedir(), ".t3", "dev"); |
| 6 | const MODE_ARGS = { | 10 | const MODE_ARGS = { |
| 7 | dev: [ | 11 | dev: [ |
| 8 | "run", | 12 | "run", |
| ... | @@ -17,6 +21,21 @@ const MODE_ARGS = { | ... | @@ -17,6 +21,21 @@ const MODE_ARGS = { |
| 17 | "dev:web": ["run", "dev", "--filter=@t3tools/web"], | 21 | "dev:web": ["run", "dev", "--filter=@t3tools/web"], |
| 18 | "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], | 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 | function parseInteger(value, envName) { | 40 | function parseInteger(value, envName) { |
| 22 | const parsed = Number(value); | 41 | const parsed = Number(value); |
| ... | @@ -35,8 +54,8 @@ function hashSeed(seed) { | ... | @@ -35,8 +54,8 @@ function hashSeed(seed) { |
| 35 | return hash >>> 0; | 54 | return hash >>> 0; |
| 36 | } | 55 | } |
| 37 | | 56 | |
| 38 | function resolveOffset() { | 57 | export function resolveOffset(env = process.env) { |
| 39 | const explicitOffset = process.env.T3CODE_PORT_OFFSET?.trim(); | 58 | const explicitOffset = env.T3CODE_PORT_OFFSET?.trim(); |
| 40 | if (explicitOffset) { | 59 | if (explicitOffset) { |
| 41 | const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); | 60 | const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); |
| 42 | if (parsed < 0) { | 61 | if (parsed < 0) { |
| ... | @@ -45,7 +64,7 @@ function resolveOffset() { | ... | @@ -45,7 +64,7 @@ function resolveOffset() { |
| 45 | return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` }; | 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 | if (!seed) { | 68 | if (!seed) { |
| 50 | return { offset: 0, source: "default ports" }; | 69 | return { offset: 0, source: "default ports" }; |
| 51 | } | 70 | } |
| ... | @@ -58,55 +77,159 @@ function resolveOffset() { | ... | @@ -58,55 +77,159 @@ function resolveOffset() { |
| 58 | return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; | 77 | return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; |
| 59 | } | 78 | } |
| 60 | | 79 | |
| 61 | function main() { | 80 | function parseLongFlag(raw) { |
| 62 | const mode = process.argv[2]; | 81 | if (!raw.startsWith("--")) return null; |
| 63 | const isDryRun = process.argv.includes("--dry-run"); | 82 | const normalized = raw.slice(2); |
| 64 | if (!mode || !(mode in MODE_ARGS)) { | 83 | const equalsIndex = normalized.indexOf("="); |
| 65 | const supportedModes = Object.keys(MODE_ARGS).join(", "); | 84 | if (equalsIndex === -1) { |
| 66 | throw new Error(`Usage: bun scripts/dev-runner.mjs <mode>. Supported modes: ${supportedModes}`); | 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(); | 93 | function toBooleanEnvValue(value) { |
| 70 | const serverPort = BASE_SERVER_PORT + offset; | 94 | if (value === undefined) return "1"; |
| 71 | const webPort = BASE_WEB_PORT + offset; | 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) { | 100 | export function parseDevRunnerArgs(rawArgs) { |
| 74 | throw new Error( | 101 | const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs.slice(); |
| 75 | `Port offset too large (${offset}). Computed ports: server=${serverPort}, web=${webPort}`, | 102 | const envOverrides = {}; |
| 76 | ); | 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 = { | 147 | return { |
| 80 | ...process.env, | 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 | T3CODE_PORT: String(serverPort), | 164 | T3CODE_PORT: String(serverPort), |
| 82 | PORT: String(webPort), | 165 | PORT: String(webPort), |
| 83 | ELECTRON_RENDERER_PORT: String(webPort), | 166 | ELECTRON_RENDERER_PORT: String(webPort), |
| 84 | VITE_WS_URL: `ws://localhost:${serverPort}`, | 167 | VITE_WS_URL: `ws://localhost:${serverPort}`, |
| 85 | VITE_DEV_SERVER_URL: `http://localhost:${webPort}`, | 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 | if (mode === "dev" || mode === "dev:server" || mode === "dev:web") { | 179 | if (mode === "dev" || mode === "dev:server" || mode === "dev:web") { |
| 89 | // Running server/web in browser mode should not inherit desktop launcher state. | 180 | // Running server/web in browser mode should not inherit desktop launcher state. |
| 90 | env.T3CODE_MODE = "web"; | 181 | output.T3CODE_MODE = "web"; |
| 91 | delete env.T3CODE_NO_BROWSER; | 182 | if (!("T3CODE_NO_BROWSER" in envOverrides)) { |
| 92 | delete env.T3CODE_AUTH_TOKEN; | 183 | delete output.T3CODE_NO_BROWSER; |
| 93 | delete env.T3CODE_DESKTOP_WS_URL; | 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) { | 206 | const { offset, source } = resolveOffset(); |
| 97 | env.T3CODE_LOG_WS_EVENTS = "1"; | 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 | console.info( | 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 | return; | 228 | return; |
| 106 | } | 229 | } |
| 107 | | 230 | |
| 108 | const command = process.platform === "win32" ? "turbo.cmd" : "turbo"; | 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 | stdio: "inherit", | 233 | stdio: "inherit", |
| 111 | env, | 234 | env, |
| 112 | }); | 235 | }); |
| ... | @@ -125,9 +248,13 @@ function main() { | ... | @@ -125,9 +248,13 @@ function main() { |
| 125 | }); | 248 | }); |
| 126 | } | 249 | } |
| 127 | | 250 | |
| 128 | try { | 251 | const entrypointPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : ""; |
| 129 | main(); | 252 | |
| 130 | } catch (error) { | 253 | if (import.meta.url === entrypointPath) { |
| 131 | console.error("[dev-runner]", error); | 254 | try { |
| 132 | process.exit(1); | 255 | runDevRunner(); |
| | 256 | } catch (error) { |
| | 257 | console.error("[dev-runner]", error); |
| | 258 | process.exit(1); |
| | 259 | } |
| 133 | } | 260 | } |