From 0451b503a387692339c4e6633580d69009c21740 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 18:01:14 -0800 Subject: [PATCH] Isolate dev state and forward server flags in dev runner - default root dev commands to `T3CODE_STATE_DIR=~/.t3/dev` - add `--` flag forwarding in `scripts/dev-runner.mjs` for server env flags - resolve desktop state dir from `T3CODE_STATE_DIR` with new tests for desktop and dev runner --- README.md | 3 + apps/desktop/src/main.ts | 3 +- apps/desktop/src/stateDir.test.ts | 23 ++++ apps/desktop/src/stateDir.ts | 12 ++ scripts/dev-runner.mjs | 195 ++++++++++++++++++++++++------ scripts/dev-runner.test.ts | 86 +++++++++++++ 6 files changed, 287 insertions(+), 35 deletions(-) create mode 100644 apps/desktop/src/stateDir.test.ts create mode 100644 apps/desktop/src/stateDir.ts create mode 100644 scripts/dev-runner.test.ts diff --git a/README.md b/README.md index 0a863d8c42e8e0a991f8b7ca62964c8344134e9f..26f95f802cd2f835061c524f85b10644e928581f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ npx t3 - `bun run dev` — Starts contracts, server, and web in `turbo watch` mode. - `bun run dev:server` — Starts just the WebSocket server (uses Bun TypeScript execution). - `bun run dev:web` — Starts just the Vite dev server for the web app. +- Dev commands default `T3CODE_STATE_DIR` to `~/.t3/dev` to keep dev state isolated from desktop/prod state. +- Override server CLI-equivalent flags from root dev commands with `--`, for example: + `bun run dev -- --state-dir ~/.t3/another-dev-state` - `bun run start` — Runs the production server (serves built web app as static files). - `bun run build` — Builds contracts, web app, and server through Turbo. - `bun run typecheck` — Strict TypeScript checks for all packages. diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c4af043591bce469c0cd040dfe39767ebf8c9fe2..c0a7b4bb2bc42d0fedc4030184b15d060bc162ac 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -18,6 +18,7 @@ import { import { showDesktopConfirmDialog } from "./confirmDialog"; import { fixPath } from "./fixPath"; +import { resolveDesktopStateDir } from "./stateDir"; fixPath(); @@ -26,7 +27,7 @@ const CONFIRM_CHANNEL = "desktop:confirm"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; const MENU_ACTION_CHANNEL = "desktop:menu-action"; -const STATE_DIR = path.join(os.homedir(), ".t3", "userdata"); +const STATE_DIR = resolveDesktopStateDir(); const DESKTOP_SCHEME = "t3"; const ROOT_DIR = path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); diff --git a/apps/desktop/src/stateDir.test.ts b/apps/desktop/src/stateDir.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d52f436b4444cac8d5cd212abdf8d0caa427ad9 --- /dev/null +++ b/apps/desktop/src/stateDir.test.ts @@ -0,0 +1,23 @@ +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { DEFAULT_DESKTOP_STATE_DIR, resolveDesktopStateDir } from "./stateDir"; + +describe("resolveDesktopStateDir", () => { + it("returns env override when provided", () => { + const resolved = resolveDesktopStateDir({ + T3CODE_STATE_DIR: " /tmp/t3-dev-state ", + }); + + expect(resolved).toBe("/tmp/t3-dev-state"); + }); + + it("falls back to default userdata dir when override is missing", () => { + const resolved = resolveDesktopStateDir({}); + + expect(resolved).toBe(DEFAULT_DESKTOP_STATE_DIR); + expect(resolved).toBe(path.join(os.homedir(), ".t3", "userdata")); + }); +}); diff --git a/apps/desktop/src/stateDir.ts b/apps/desktop/src/stateDir.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8fb37db80193244f50df166977900daad166668 --- /dev/null +++ b/apps/desktop/src/stateDir.ts @@ -0,0 +1,12 @@ +import os from "node:os"; +import path from "node:path"; + +export const DEFAULT_DESKTOP_STATE_DIR = path.join(os.homedir(), ".t3", "userdata"); + +export function resolveDesktopStateDir(env: NodeJS.ProcessEnv = process.env): string { + const value = env.T3CODE_STATE_DIR?.trim(); + if (value) { + return value; + } + return DEFAULT_DESKTOP_STATE_DIR; +} diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index dce79715c9740632a8c7e822912f24513ca44ae6..67ffbc2b6937f2e14238bda30a7bf57195592e1a 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -1,8 +1,12 @@ import { spawn } from "node:child_process"; +import { homedir } from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; const BASE_SERVER_PORT = 3773; const BASE_WEB_PORT = 5173; const MAX_HASH_OFFSET = 3000; +export const DEFAULT_DEV_STATE_DIR = path.join(homedir(), ".t3", "dev"); const MODE_ARGS = { dev: [ "run", @@ -17,6 +21,21 @@ const MODE_ARGS = { "dev:web": ["run", "dev", "--filter=@t3tools/web"], "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], }; +const FORWARDED_ENV_FLAGS = { + "state-dir": { envName: "T3CODE_STATE_DIR", expectsValue: true }, + "auth-token": { envName: "T3CODE_AUTH_TOKEN", expectsValue: true }, + token: { envName: "T3CODE_AUTH_TOKEN", expectsValue: true }, + "no-browser": { envName: "T3CODE_NO_BROWSER", expectsValue: false }, + "auto-bootstrap-project-from-cwd": { + envName: "T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD", + expectsValue: false, + }, + "log-websocket-events": { envName: "T3CODE_LOG_WS_EVENTS", expectsValue: false }, + "log-ws-events": { envName: "T3CODE_LOG_WS_EVENTS", expectsValue: false }, + host: { envName: "T3CODE_HOST", expectsValue: true }, + port: { envName: "T3CODE_PORT", expectsValue: true }, + "dev-url": { envName: "VITE_DEV_SERVER_URL", expectsValue: true }, +}; function parseInteger(value, envName) { const parsed = Number(value); @@ -35,8 +54,8 @@ function hashSeed(seed) { return hash >>> 0; } -function resolveOffset() { - const explicitOffset = process.env.T3CODE_PORT_OFFSET?.trim(); +export function resolveOffset(env = process.env) { + const explicitOffset = env.T3CODE_PORT_OFFSET?.trim(); if (explicitOffset) { const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); if (parsed < 0) { @@ -45,7 +64,7 @@ function resolveOffset() { return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` }; } - const seed = process.env.T3CODE_DEV_INSTANCE?.trim(); + const seed = env.T3CODE_DEV_INSTANCE?.trim(); if (!seed) { return { offset: 0, source: "default ports" }; } @@ -58,9 +77,127 @@ function resolveOffset() { return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; } -function main() { - const mode = process.argv[2]; - const isDryRun = process.argv.includes("--dry-run"); +function parseLongFlag(raw) { + if (!raw.startsWith("--")) return null; + const normalized = raw.slice(2); + const equalsIndex = normalized.indexOf("="); + if (equalsIndex === -1) { + return { name: normalized, value: undefined }; + } + return { + name: normalized.slice(0, equalsIndex), + value: normalized.slice(equalsIndex + 1), + }; +} + +function toBooleanEnvValue(value) { + if (value === undefined) return "1"; + const normalized = value.trim().toLowerCase(); + if (normalized === "false" || normalized === "0" || normalized === "no") return "0"; + return "1"; +} + +export function parseDevRunnerArgs(rawArgs) { + const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs.slice(); + const envOverrides = {}; + const turboArgs = []; + let isDryRun = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--dry-run") { + isDryRun = true; + continue; + } + + if (arg === "--") { + turboArgs.push(...args.slice(index + 1)); + break; + } + + const parsedFlag = parseLongFlag(arg); + if (!parsedFlag) { + turboArgs.push(arg); + continue; + } + + const definition = FORWARDED_ENV_FLAGS[parsedFlag.name]; + if (!definition) { + turboArgs.push(arg); + continue; + } + + if (definition.expectsValue) { + let value = parsedFlag.value; + if (value === undefined) { + const next = args[index + 1]; + if (next === undefined) { + throw new Error(`Missing value for --${parsedFlag.name}`); + } + value = next; + index += 1; + } + envOverrides[definition.envName] = value; + continue; + } + + envOverrides[definition.envName] = toBooleanEnvValue(parsedFlag.value); + } + + return { + isDryRun, + envOverrides, + turboArgs, + }; +} + +function resolveStateDir(envOverrides) { + return envOverrides.T3CODE_STATE_DIR ?? DEFAULT_DEV_STATE_DIR; +} + +export function createDevRunnerEnv({ mode, env, offset, envOverrides }) { + const serverPort = BASE_SERVER_PORT + offset; + const webPort = BASE_WEB_PORT + offset; + + const output = { + ...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}`, + ...envOverrides, + }; + + output.T3CODE_STATE_DIR = resolveStateDir(envOverrides); + + const parsedServerPort = Number(output.T3CODE_PORT); + if (Number.isInteger(parsedServerPort) && parsedServerPort > 0 && parsedServerPort <= 65535) { + output.VITE_WS_URL = `ws://localhost:${parsedServerPort}`; + } + + if (mode === "dev" || mode === "dev:server" || mode === "dev:web") { + // Running server/web in browser mode should not inherit desktop launcher state. + output.T3CODE_MODE = "web"; + if (!("T3CODE_NO_BROWSER" in envOverrides)) { + delete output.T3CODE_NO_BROWSER; + } + if (!("T3CODE_AUTH_TOKEN" in envOverrides)) { + delete output.T3CODE_AUTH_TOKEN; + } + delete output.T3CODE_DESKTOP_WS_URL; + } + + if (mode === "dev" && !output.T3CODE_LOG_WS_EVENTS) { + output.T3CODE_LOG_WS_EVENTS = "1"; + } + + return output; +} + +export function runDevRunner(argv = process.argv) { + const mode = argv[2]; + const parsedArgs = parseDevRunnerArgs(argv.slice(3)); 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}`); @@ -76,37 +213,23 @@ function main() { ); } - 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" || mode === "dev:server" || mode === "dev:web") { - // Running server/web in browser mode should not inherit desktop launcher state. - env.T3CODE_MODE = "web"; - delete env.T3CODE_NO_BROWSER; - delete env.T3CODE_AUTH_TOKEN; - delete env.T3CODE_DESKTOP_WS_URL; - } - - if (mode === "dev" && !env.T3CODE_LOG_WS_EVENTS) { - env.T3CODE_LOG_WS_EVENTS = "1"; - } + const env = createDevRunnerEnv({ + mode, + env: process.env, + offset, + envOverrides: parsedArgs.envOverrides, + }); console.info( - `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort}`, + `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort} stateDir=${env.T3CODE_STATE_DIR}`, ); - if (isDryRun) { + if (parsedArgs.isDryRun) { return; } const command = process.platform === "win32" ? "turbo.cmd" : "turbo"; - const child = spawn(command, MODE_ARGS[mode], { + const child = spawn(command, [...MODE_ARGS[mode], ...parsedArgs.turboArgs], { stdio: "inherit", env, }); @@ -125,9 +248,13 @@ function main() { }); } -try { - main(); -} catch (error) { - console.error("[dev-runner]", error); - process.exit(1); +const entrypointPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : ""; + +if (import.meta.url === entrypointPath) { + try { + runDevRunner(); + } catch (error) { + console.error("[dev-runner]", error); + process.exit(1); + } } diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..98dd5bd53be814263870692c7a241bf2378757f0 --- /dev/null +++ b/scripts/dev-runner.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import { + DEFAULT_DEV_STATE_DIR, + createDevRunnerEnv, + parseDevRunnerArgs, +} from "./dev-runner.mjs"; + +describe("parseDevRunnerArgs", () => { + it("maps supported server flags to env overrides and keeps other flags for turbo", () => { + const parsed = parseDevRunnerArgs([ + "--", + "--state-dir", + "~/alt-state", + "--token=abc123", + "--no-browser=false", + "--dry-run", + "--continue", + ]); + + expect(parsed.isDryRun).toBe(true); + expect(parsed.envOverrides).toEqual({ + T3CODE_STATE_DIR: "~/alt-state", + T3CODE_AUTH_TOKEN: "abc123", + T3CODE_NO_BROWSER: "0", + }); + expect(parsed.turboArgs).toEqual(["--continue"]); + }); + + it("throws when a value flag is missing its value", () => { + expect(() => parseDevRunnerArgs(["--state-dir"])).toThrow("Missing value for --state-dir"); + }); +}); + +describe("createDevRunnerEnv", () => { + it("defaults state dir to ~/.t3/dev when not provided", () => { + const env = createDevRunnerEnv({ + mode: "dev", + env: { T3CODE_STATE_DIR: "/tmp/prod-state" }, + offset: 0, + envOverrides: {}, + }); + + expect(env.T3CODE_STATE_DIR).toBe(DEFAULT_DEV_STATE_DIR); + }); + + it("lets --state-dir override existing env state dir", () => { + const env = createDevRunnerEnv({ + mode: "dev:server", + env: { T3CODE_STATE_DIR: "/tmp/existing-state" }, + offset: 0, + envOverrides: { T3CODE_STATE_DIR: "/tmp/override-state" }, + }); + + expect(env.T3CODE_STATE_DIR).toBe("/tmp/override-state"); + }); + + it("recomputes websocket url when port is overridden", () => { + const env = createDevRunnerEnv({ + mode: "dev", + env: {}, + offset: 0, + envOverrides: { T3CODE_PORT: "4222" }, + }); + + expect(env.T3CODE_PORT).toBe("4222"); + expect(env.VITE_WS_URL).toBe("ws://localhost:4222"); + }); + + it("keeps explicitly forwarded web-mode flags", () => { + const env = createDevRunnerEnv({ + mode: "dev", + env: { + T3CODE_AUTH_TOKEN: "desktop-token", + }, + offset: 0, + envOverrides: { + T3CODE_NO_BROWSER: "1", + T3CODE_AUTH_TOKEN: "cli-token", + }, + }); + + expect(env.T3CODE_NO_BROWSER).toBe("1"); + expect(env.T3CODE_AUTH_TOKEN).toBe("cli-token"); + }); +}); -- 2.54.0