| author | |
| committer | |
| log | 0c4c027e92bae27716d94d14e3531c390d9ecef1 |
| tree | 0882fdf8fb3966279ec85163e5f3a2a85b273998 |
| parent | fefea0bdf6b09a596b768047bc8432af48ba1123 |
5 files changed, 99 insertions(+), 60 deletions(-)
apps/server/src/config.ts+2| ... | @@ -16,6 +16,8 @@ export interface ServerConfigShape { | ... | @@ -16,6 +16,8 @@ export interface ServerConfigShape { |
| 16 | readonly devUrl: URL | undefined; | 16 | readonly devUrl: URL | undefined; |
| 17 | readonly noBrowser: boolean; | 17 | readonly noBrowser: boolean; |
| 18 | readonly authToken: string | undefined; | 18 | readonly authToken: string | undefined; |
| 19 | readonly autoBootstrapProjectFromCwd: boolean; | ||
| 20 | readonly logWebSocketEvents: boolean; | ||
| 19 | } | 21 | } |
| 20 | 22 | ||
| 21 | export class ServerConfig extends ServiceMap.Service<ServerConfig, ServerConfigShape>()( | 23 | export class ServerConfig extends ServiceMap.Service<ServerConfig, ServerConfigShape>()( |
apps/server/src/main.test.ts+36-33| ... | @@ -10,20 +10,18 @@ import { beforeEach } from "vitest"; | ... | @@ -10,20 +10,18 @@ import { beforeEach } from "vitest"; |
| 10 | import { CliConfig, t3Cli, type CliConfigShape } from "./main"; | 10 | import { CliConfig, t3Cli, type CliConfigShape } from "./main"; |
| 11 | import { NetService, ServerConfig, type ServerConfigShape } from "./config"; | 11 | import { NetService, ServerConfig, type ServerConfigShape } from "./config"; |
| 12 | import { Open, type OpenShape } from "./open"; | 12 | import { Open, type OpenShape } from "./open"; |
| 13 | import { Server, type ServerShape, type ServerOptions } from "./wsServer"; | 13 | import { Server, type ServerShape } from "./wsServer"; |
| 14 | 14 | ||
| 15 | const start = vi.fn(() => undefined); | 15 | const start = vi.fn(() => undefined); |
| 16 | const stop = vi.fn(() => undefined); | 16 | const stop = vi.fn(() => undefined); |
| 17 | let resolvedConfig: ServerConfigShape | null = null; | 17 | let resolvedConfig: ServerConfigShape | null = null; |
| 18 | const createServer = vi.fn((_: ServerOptions | undefined) => | 18 | const serverStart = Effect.acquireRelease( |
| 19 | Effect.acquireRelease( | 19 | Effect.gen(function* () { |
| 20 | Effect.gen(function* () { | 20 | resolvedConfig = yield* ServerConfig; |
| 21 | resolvedConfig = yield* ServerConfig; | 21 | start(); |
| 22 | start(); | 22 | return {} as unknown as Http.Server; |
| 23 | return {} as unknown as Http.Server; | 23 | }), |
| 24 | }), | 24 | () => Effect.sync(() => stop()), |
| 25 | () => Effect.sync(() => stop()), | ||
| 26 | ), | ||
| 27 | ); | 25 | ); |
| 28 | const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred)); | 26 | const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred)); |
| 29 | 27 | ||
| ... | @@ -37,7 +35,7 @@ const testLayer = Layer.mergeAll( | ... | @@ -37,7 +35,7 @@ const testLayer = Layer.mergeAll( |
| 37 | findAvailablePort, | 35 | findAvailablePort, |
| 38 | }), | 36 | }), |
| 39 | Layer.succeed(Server, { | 37 | Layer.succeed(Server, { |
| 40 | createServer, | 38 | start: serverStart, |
| 41 | stopSignal: Effect.void, | 39 | stopSignal: Effect.void, |
| 42 | } satisfies ServerShape), | 40 | } satisfies ServerShape), |
| 43 | Layer.succeed(Open, { | 41 | Layer.succeed(Open, { |
| ... | @@ -60,16 +58,6 @@ beforeEach(() => { | ... | @@ -60,16 +58,6 @@ beforeEach(() => { |
| 60 | resolvedConfig = null; | 58 | resolvedConfig = null; |
| 61 | start.mockImplementation(() => undefined); | 59 | start.mockImplementation(() => undefined); |
| 62 | stop.mockImplementation(() => undefined); | 60 | stop.mockImplementation(() => undefined); |
| 63 | createServer.mockImplementation((_: ServerOptions | undefined) => | ||
| 64 | Effect.acquireRelease( | ||
| 65 | Effect.gen(function* () { | ||
| 66 | resolvedConfig = yield* ServerConfig; | ||
| 67 | start(); | ||
| 68 | return {} as unknown as Http.Server; | ||
| 69 | }), | ||
| 70 | () => Effect.sync(() => stop()), | ||
| 71 | ), | ||
| 72 | ); | ||
| 73 | findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); | 61 | findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); |
| 74 | }); | 62 | }); |
| 75 | 63 | ||
| ... | @@ -92,7 +80,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -92,7 +80,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 92 | "auth-secret", | 80 | "auth-secret", |
| 93 | ]); | 81 | ]); |
| 94 | 82 | ||
| 95 | assert.equal(createServer.mock.calls.length, 1); | 83 | assert.equal(start.mock.calls.length, 1); |
| 96 | assert.equal(resolvedConfig?.mode, "desktop"); | 84 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 97 | assert.equal(resolvedConfig?.port, 4010); | 85 | assert.equal(resolvedConfig?.port, 4010); |
| 98 | assert.equal(resolvedConfig?.host, "0.0.0.0"); | 86 | assert.equal(resolvedConfig?.host, "0.0.0.0"); |
| ... | @@ -100,7 +88,8 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -100,7 +88,8 @@ it.layer(testLayer)("server cli", (it) => { |
| 100 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/"); | 88 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/"); |
| 101 | assert.equal(resolvedConfig?.noBrowser, true); | 89 | assert.equal(resolvedConfig?.noBrowser, true); |
| 102 | assert.equal(resolvedConfig?.authToken, "auth-secret"); | 90 | assert.equal(resolvedConfig?.authToken, "auth-secret"); |
| 103 | assert.equal(start.mock.calls.length, 1); | 91 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); |
| 92 | assert.equal(resolvedConfig?.logWebSocketEvents, true); | ||
| 104 | assert.equal(stop.mock.calls.length, 1); | 93 | assert.equal(stop.mock.calls.length, 1); |
| 105 | }), | 94 | }), |
| 106 | ); | 95 | ); |
| ... | @@ -109,7 +98,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -109,7 +98,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 109 | Effect.gen(function* () { | 98 | Effect.gen(function* () { |
| 110 | yield* runCli(["--token", "token-secret"]); | 99 | yield* runCli(["--token", "token-secret"]); |
| 111 | 100 | ||
| 112 | assert.equal(createServer.mock.calls.length, 1); | 101 | assert.equal(start.mock.calls.length, 1); |
| 113 | assert.equal(resolvedConfig?.authToken, "token-secret"); | 102 | assert.equal(resolvedConfig?.authToken, "token-secret"); |
| 114 | }), | 103 | }), |
| 115 | ); | 104 | ); |
| ... | @@ -126,7 +115,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -126,7 +115,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 126 | T3CODE_AUTH_TOKEN: "env-token", | 115 | T3CODE_AUTH_TOKEN: "env-token", |
| 127 | }); | 116 | }); |
| 128 | 117 | ||
| 129 | assert.equal(createServer.mock.calls.length, 1); | 118 | assert.equal(start.mock.calls.length, 1); |
| 130 | assert.equal(resolvedConfig?.mode, "desktop"); | 119 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 131 | assert.equal(resolvedConfig?.port, 4999); | 120 | assert.equal(resolvedConfig?.port, 4999); |
| 132 | assert.equal(resolvedConfig?.host, "100.88.10.4"); | 121 | assert.equal(resolvedConfig?.host, "100.88.10.4"); |
| ... | @@ -134,6 +123,8 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -134,6 +123,8 @@ it.layer(testLayer)("server cli", (it) => { |
| 134 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://localhost:5173/"); | 123 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://localhost:5173/"); |
| 135 | assert.equal(resolvedConfig?.noBrowser, true); | 124 | assert.equal(resolvedConfig?.noBrowser, true); |
| 136 | assert.equal(resolvedConfig?.authToken, "env-token"); | 125 | assert.equal(resolvedConfig?.authToken, "env-token"); |
| 126 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); | ||
| 127 | assert.equal(resolvedConfig?.logWebSocketEvents, true); | ||
| 137 | assert.equal(findAvailablePort.mock.calls.length, 0); | 128 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 138 | }), | 129 | }), |
| 139 | ); | 130 | ); |
| ... | @@ -147,7 +138,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -147,7 +138,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 147 | }); | 138 | }); |
| 148 | 139 | ||
| 149 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); | 140 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); |
| 150 | assert.equal(createServer.mock.calls.length, 1); | 141 | assert.equal(start.mock.calls.length, 1); |
| 151 | assert.equal(resolvedConfig?.mode, "web"); | 142 | assert.equal(resolvedConfig?.mode, "web"); |
| 152 | assert.equal(resolvedConfig?.port, 4666); | 143 | assert.equal(resolvedConfig?.port, 4666); |
| 153 | assert.equal(resolvedConfig?.host, undefined); | 144 | assert.equal(resolvedConfig?.host, undefined); |
| ... | @@ -160,7 +151,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -160,7 +151,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 160 | T3CODE_NO_BROWSER: "false", | 151 | T3CODE_NO_BROWSER: "false", |
| 161 | }); | 152 | }); |
| 162 | 153 | ||
| 163 | assert.equal(createServer.mock.calls.length, 1); | 154 | assert.equal(start.mock.calls.length, 1); |
| 164 | assert.equal(resolvedConfig?.noBrowser, true); | 155 | assert.equal(resolvedConfig?.noBrowser, true); |
| 165 | }), | 156 | }), |
| 166 | ); | 157 | ); |
| ... | @@ -171,7 +162,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -171,7 +162,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 171 | yield* runCli([]); | 162 | yield* runCli([]); |
| 172 | 163 | ||
| 173 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); | 164 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); |
| 174 | assert.equal(createServer.mock.calls.length, 1); | 165 | assert.equal(start.mock.calls.length, 1); |
| 175 | assert.equal(resolvedConfig?.port, 5444); | 166 | assert.equal(resolvedConfig?.port, 5444); |
| 176 | assert.equal(resolvedConfig?.mode, "web"); | 167 | assert.equal(resolvedConfig?.mode, "web"); |
| 177 | }), | 168 | }), |
| ... | @@ -185,7 +176,7 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -185,7 +176,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 185 | }); | 176 | }); |
| 186 | 177 | ||
| 187 | assert.equal(findAvailablePort.mock.calls.length, 0); | 178 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 188 | assert.equal(createServer.mock.calls.length, 1); | 179 | assert.equal(start.mock.calls.length, 1); |
| 189 | assert.equal(resolvedConfig?.port, 3773); | 180 | assert.equal(resolvedConfig?.port, 3773); |
| 190 | assert.equal(resolvedConfig?.host, "127.0.0.1"); | 181 | assert.equal(resolvedConfig?.host, "127.0.0.1"); |
| 191 | assert.equal(resolvedConfig?.mode, "desktop"); | 182 | assert.equal(resolvedConfig?.mode, "desktop"); |
| ... | @@ -199,17 +190,31 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -199,17 +190,31 @@ it.layer(testLayer)("server cli", (it) => { |
| 199 | T3CODE_NO_BROWSER: "true", | 190 | T3CODE_NO_BROWSER: "true", |
| 200 | }); | 191 | }); |
| 201 | 192 | ||
| 202 | assert.equal(createServer.mock.calls.length, 1); | 193 | assert.equal(start.mock.calls.length, 1); |
| 203 | assert.equal(resolvedConfig?.mode, "desktop"); | 194 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 204 | assert.equal(resolvedConfig?.host, "0.0.0.0"); | 195 | assert.equal(resolvedConfig?.host, "0.0.0.0"); |
| 205 | }), | 196 | }), |
| 206 | ); | 197 | ); |
| 207 | 198 | ||
| 199 | it.effect("supports CLI and env for bootstrap/log websocket toggles", () => | ||
| 200 | Effect.gen(function* () { | ||
| 201 | yield* runCli(["--auto-bootstrap-project-from-cwd"], { | ||
| 202 | T3CODE_MODE: "desktop", | ||
| 203 | T3CODE_LOG_WS_EVENTS: "false", | ||
| 204 | T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false", | ||
| 205 | T3CODE_NO_BROWSER: "true", | ||
| 206 | }); | ||
| 207 | |||
| 208 | assert.equal(start.mock.calls.length, 1); | ||
| 209 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, true); | ||
| 210 | assert.equal(resolvedConfig?.logWebSocketEvents, false); | ||
| 211 | }), | ||
| 212 | ); | ||
| 213 | |||
| 208 | it.effect("does not start server for invalid --mode values", () => | 214 | it.effect("does not start server for invalid --mode values", () => |
| 209 | Effect.gen(function* () { | 215 | Effect.gen(function* () { |
| 210 | yield* runCli(["--mode", "invalid"]); | 216 | yield* runCli(["--mode", "invalid"]); |
| 211 | 217 | ||
| 212 | assert.equal(createServer.mock.calls.length, 0); | ||
| 213 | assert.equal(start.mock.calls.length, 0); | 218 | assert.equal(start.mock.calls.length, 0); |
| 214 | assert.equal(stop.mock.calls.length, 0); | 219 | assert.equal(stop.mock.calls.length, 0); |
| 215 | }), | 220 | }), |
| ... | @@ -219,7 +224,6 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -219,7 +224,6 @@ it.layer(testLayer)("server cli", (it) => { |
| 219 | Effect.gen(function* () { | 224 | Effect.gen(function* () { |
| 220 | yield* runCli(["--dev-url", "not-a-url"]).pipe(Effect.catch(() => Effect.void)); | 225 | yield* runCli(["--dev-url", "not-a-url"]).pipe(Effect.catch(() => Effect.void)); |
| 221 | 226 | ||
| 222 | assert.equal(createServer.mock.calls.length, 0); | ||
| 223 | assert.equal(start.mock.calls.length, 0); | 227 | assert.equal(start.mock.calls.length, 0); |
| 224 | assert.equal(stop.mock.calls.length, 0); | 228 | assert.equal(stop.mock.calls.length, 0); |
| 225 | }), | 229 | }), |
| ... | @@ -230,7 +234,6 @@ it.layer(testLayer)("server cli", (it) => { | ... | @@ -230,7 +234,6 @@ it.layer(testLayer)("server cli", (it) => { |
| 230 | yield* runCli(["--port", "70000"]); | 234 | yield* runCli(["--port", "70000"]); |
| 231 | 235 | ||
| 232 | // effect/unstable/cli renders help/errors for parse failures and returns success. | 236 | // effect/unstable/cli renders help/errors for parse failures and returns success. |
| 233 | assert.equal(createServer.mock.calls.length, 0); | ||
| 234 | assert.equal(start.mock.calls.length, 0); | 237 | assert.equal(start.mock.calls.length, 0); |
| 235 | assert.equal(stop.mock.calls.length, 0); | 238 | assert.equal(stop.mock.calls.length, 0); |
| 236 | }), | 239 | }), |
apps/server/src/main.ts+41-2| ... | @@ -28,6 +28,8 @@ interface CliInput { | ... | @@ -28,6 +28,8 @@ interface CliInput { |
| 28 | readonly devUrl: Option.Option<URL>; | 28 | readonly devUrl: Option.Option<URL>; |
| 29 | readonly noBrowser: Option.Option<boolean>; | 29 | readonly noBrowser: Option.Option<boolean>; |
| 30 | readonly authToken: Option.Option<string>; | 30 | readonly authToken: Option.Option<string>; |
| 31 | readonly autoBootstrapProjectFromCwd: Option.Option<boolean>; | ||
| 32 | readonly logWebSocketEvents: Option.Option<boolean>; | ||
| 31 | } | 33 | } |
| 32 | 34 | ||
| 33 | export interface CliConfigShape { | 35 | export interface CliConfigShape { |
| ... | @@ -79,6 +81,14 @@ const CliEnvConfig = Config.all({ | ... | @@ -79,6 +81,14 @@ const CliEnvConfig = Config.all({ |
| 79 | Config.option, | 81 | Config.option, |
| 80 | Config.map(Option.getOrUndefined), | 82 | Config.map(Option.getOrUndefined), |
| 81 | ), | 83 | ), |
| 84 | autoBootstrapProjectFromCwd: Config.boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( | ||
| 85 | Config.option, | ||
| 86 | Config.map(Option.getOrUndefined), | ||
| 87 | ), | ||
| 88 | logWebSocketEvents: Config.boolean("T3CODE_LOG_WS_EVENTS").pipe( | ||
| 89 | Config.option, | ||
| 90 | Config.map(Option.getOrUndefined), | ||
| 91 | ), | ||
| 82 | }); | 92 | }); |
| 83 | 93 | ||
| 84 | const ServerConfigLive = (input: CliInput) => | 94 | const ServerConfigLive = (input: CliInput) => |
| ... | @@ -119,6 +129,18 @@ const ServerConfigLive = (input: CliInput) => | ... | @@ -119,6 +129,18 @@ const ServerConfigLive = (input: CliInput) => |
| 119 | onNone: () => env.noBrowser ?? mode === "desktop", | 129 | onNone: () => env.noBrowser ?? mode === "desktop", |
| 120 | }); | 130 | }); |
| 121 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; | 131 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; |
| 132 | const autoBootstrapProjectFromCwd = Option.match(input.autoBootstrapProjectFromCwd, { | ||
| 133 | // effect/cli boolean flags parse to `false` when absent; in that case | ||
| 134 | // we still want env/mode fallbacks to apply. | ||
| 135 | onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? mode === "web")), | ||
| 136 | onNone: () => env.autoBootstrapProjectFromCwd ?? mode === "web", | ||
| 137 | }); | ||
| 138 | const logWebSocketEvents = Option.match(input.logWebSocketEvents, { | ||
| 139 | // effect/cli boolean flags parse to `false` when absent; in that case | ||
| 140 | // we still want env/dev fallbacks to apply. | ||
| 141 | onSome: (value) => (value ? true : (env.logWebSocketEvents ?? Boolean(devUrl))), | ||
| 142 | onNone: () => env.logWebSocketEvents ?? Boolean(devUrl), | ||
| 143 | }); | ||
| 122 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; | 144 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; |
| 123 | const { join } = yield* Path.Path; | 145 | const { join } = yield* Path.Path; |
| 124 | const keybindingsConfigPath = join(stateDir, "keybindings.json"); | 146 | const keybindingsConfigPath = join(stateDir, "keybindings.json"); |
| ... | @@ -138,6 +160,8 @@ const ServerConfigLive = (input: CliInput) => | ... | @@ -138,6 +160,8 @@ const ServerConfigLive = (input: CliInput) => |
| 138 | devUrl, | 160 | devUrl, |
| 139 | noBrowser, | 161 | noBrowser, |
| 140 | authToken, | 162 | authToken, |
| 163 | autoBootstrapProjectFromCwd, | ||
| 164 | logWebSocketEvents, | ||
| 141 | } satisfies ServerConfigShape; | 165 | } satisfies ServerConfigShape; |
| 142 | }), | 166 | }), |
| 143 | ); | 167 | ); |
| ... | @@ -159,7 +183,7 @@ const formatHostForUrl = (host: string): string => | ... | @@ -159,7 +183,7 @@ const formatHostForUrl = (host: string): string => |
| 159 | const makeServerProgram = (input: CliInput) => | 183 | const makeServerProgram = (input: CliInput) => |
| 160 | Effect.gen(function* () { | 184 | Effect.gen(function* () { |
| 161 | const cliConfig = yield* CliConfig; | 185 | const cliConfig = yield* CliConfig; |
| 162 | const { createServer, stopSignal } = yield* Server; | 186 | const { start, stopSignal } = yield* Server; |
| 163 | const openDeps = yield* Open; | 187 | const openDeps = yield* Open; |
| 164 | yield* cliConfig.fixPath; | 188 | yield* cliConfig.fixPath; |
| 165 | 189 | ||
| ... | @@ -174,7 +198,7 @@ const makeServerProgram = (input: CliInput) => | ... | @@ -174,7 +198,7 @@ const makeServerProgram = (input: CliInput) => |
| 174 | ); | 198 | ); |
| 175 | } | 199 | } |
| 176 | 200 | ||
| 177 | yield* createServer(); | 201 | yield* start; |
| 178 | 202 | ||
| 179 | const localUrl = `http://localhost:${config.port}`; | 203 | const localUrl = `http://localhost:${config.port}`; |
| 180 | const bindUrl = | 204 | const bindUrl = |
| ... | @@ -240,6 +264,19 @@ const authTokenFlag = Flag.string("auth-token").pipe( | ... | @@ -240,6 +264,19 @@ const authTokenFlag = Flag.string("auth-token").pipe( |
| 240 | Flag.withAlias("token"), | 264 | Flag.withAlias("token"), |
| 241 | Flag.optional, | 265 | Flag.optional, |
| 242 | ); | 266 | ); |
| 267 | const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe( | ||
| 268 | Flag.withDescription( | ||
| 269 | "Create a project for the current working directory on startup when missing.", | ||
| 270 | ), | ||
| 271 | Flag.optional, | ||
| 272 | ); | ||
| 273 | const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( | ||
| 274 | Flag.withDescription( | ||
| 275 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", | ||
| 276 | ), | ||
| 277 | Flag.withAlias("log-ws-events"), | ||
| 278 | Flag.optional, | ||
| 279 | ); | ||
| 243 | 280 | ||
| 244 | export const t3Cli = Command.make("t3", { | 281 | export const t3Cli = Command.make("t3", { |
| 245 | mode: modeFlag, | 282 | mode: modeFlag, |
| ... | @@ -249,6 +286,8 @@ export const t3Cli = Command.make("t3", { | ... | @@ -249,6 +286,8 @@ export const t3Cli = Command.make("t3", { |
| 249 | devUrl: devUrlFlag, | 286 | devUrl: devUrlFlag, |
| 250 | noBrowser: noBrowserFlag, | 287 | noBrowser: noBrowserFlag, |
| 251 | authToken: authTokenFlag, | 288 | authToken: authTokenFlag, |
| 289 | autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, | ||
| 290 | logWebSocketEvents: logWebSocketEventsFlag, | ||
| 252 | }).pipe( | 291 | }).pipe( |
| 253 | Command.withDescription("Run the T3 Code server."), | 292 | Command.withDescription("Run the T3 Code server."), |
| 254 | Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), | 293 | Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), |
apps/server/src/wsServer.test.ts+4-3| ... | @@ -338,6 +338,7 @@ describe("WebSocket Server", () => { | ... | @@ -338,6 +338,7 @@ describe("WebSocket Server", () => { |
| 338 | persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>; | 338 | persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>; |
| 339 | cwd?: string; | 339 | cwd?: string; |
| 340 | autoBootstrapProjectFromCwd?: boolean; | 340 | autoBootstrapProjectFromCwd?: boolean; |
| 341 | logWebSocketEvents?: boolean; | ||
| 341 | devUrl?: string; | 342 | devUrl?: string; |
| 342 | authToken?: string; | 343 | authToken?: string; |
| 343 | stateDir?: string; | 344 | stateDir?: string; |
| ... | @@ -368,6 +369,8 @@ describe("WebSocket Server", () => { | ... | @@ -368,6 +369,8 @@ describe("WebSocket Server", () => { |
| 368 | devUrl: options.devUrl ? new URL(options.devUrl) : undefined, | 369 | devUrl: options.devUrl ? new URL(options.devUrl) : undefined, |
| 369 | noBrowser: true, | 370 | noBrowser: true, |
| 370 | authToken: options.authToken, | 371 | authToken: options.authToken, |
| 372 | autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, | ||
| 373 | logWebSocketEvents: options.logWebSocketEvents ?? Boolean(options.devUrl), | ||
| 371 | } satisfies ServerConfigShape); | 374 | } satisfies ServerConfigShape); |
| 372 | const infrastructureLayer = providerLayer.pipe(Layer.provideMerge(persistenceLayer)); | 375 | const infrastructureLayer = providerLayer.pipe(Layer.provideMerge(persistenceLayer)); |
| 373 | const runtimeOverrides = Layer.mergeAll( | 376 | const runtimeOverrides = Layer.mergeAll( |
| ... | @@ -397,9 +400,7 @@ describe("WebSocket Server", () => { | ... | @@ -397,9 +400,7 @@ describe("WebSocket Server", () => { |
| 397 | 400 | ||
| 398 | try { | 401 | try { |
| 399 | const runtime = await Effect.runPromise( | 402 | const runtime = await Effect.runPromise( |
| 400 | createServer({ | 403 | createServer().pipe(Effect.provide(runtimeServices), Scope.provide(scope)), |
| 401 | autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, | ||
| 402 | }).pipe(Effect.provide(runtimeServices), Scope.provide(scope)), | ||
| 403 | ); | 404 | ); |
| 404 | serverScope = scope; | 405 | serverScope = scope; |
| 405 | return runtime; | 406 | return runtime; |
apps/server/src/wsServer.ts+16-22| ... | @@ -49,15 +49,12 @@ const MIME_TYPES: Record<string, string> = { | ... | @@ -49,15 +49,12 @@ const MIME_TYPES: Record<string, string> = { |
| 49 | ".map": "application/json", | 49 | ".map": "application/json", |
| 50 | }; | 50 | }; |
| 51 | 51 | ||
| 52 | export interface ServerOptions { | ||
| 53 | autoBootstrapProjectFromCwd?: boolean | undefined; | ||
| 54 | logWebSocketEvents?: boolean | undefined; | ||
| 55 | } | ||
| 56 | |||
| 57 | export interface ServerShape { | 52 | export interface ServerShape { |
| 58 | readonly createServer: ( | 53 | readonly start: Effect.Effect< |
| 59 | options?: ServerOptions, | 54 | http.Server, |
| 60 | ) => Effect.Effect<http.Server, unknown, Scope.Scope | ServerRuntimeServices | ServerConfig>; | 55 | unknown, |
| 56 | Scope.Scope | ServerRuntimeServices | ServerConfig | ||
| 57 | >; | ||
| 61 | readonly stopSignal: Effect.Effect<unknown, never>; | 58 | readonly stopSignal: Effect.Effect<unknown, never>; |
| 62 | } | 59 | } |
| 63 | 60 | ||
| ... | @@ -82,14 +79,6 @@ function rejectUpgrade(socket: Duplex, statusCode: number, message: string): voi | ... | @@ -82,14 +79,6 @@ function rejectUpgrade(socket: Duplex, statusCode: number, message: string): voi |
| 82 | ); | 79 | ); |
| 83 | } | 80 | } |
| 84 | 81 | ||
| 85 | function parseBooleanEnv(value: string | undefined): boolean | undefined { | ||
| 86 | if (value === undefined) return undefined; | ||
| 87 | const normalized = value.trim().toLowerCase(); | ||
| 88 | if (["1", "true", "yes", "on"].includes(normalized)) return true; | ||
| 89 | if (["0", "false", "no", "off"].includes(normalized)) return false; | ||
| 90 | return undefined; | ||
| 91 | } | ||
| 92 | |||
| 93 | function websocketRawToString(raw: unknown): string | null { | 82 | function websocketRawToString(raw: unknown): string | null { |
| 94 | if (typeof raw === "string") { | 83 | if (typeof raw === "string") { |
| 95 | return raw; | 84 | return raw; |
| ... | @@ -153,17 +142,22 @@ class RouteRequestError extends Schema.TaggedErrorClass<RouteRequestError>()("Ro | ... | @@ -153,17 +142,22 @@ class RouteRequestError extends Schema.TaggedErrorClass<RouteRequestError>()("Ro |
| 153 | message: Schema.String, | 142 | message: Schema.String, |
| 154 | }) {} | 143 | }) {} |
| 155 | 144 | ||
| 156 | export const createServer = Effect.fn(function* (options: ServerOptions = {}): Effect.fn.Return< | 145 | export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 157 | http.Server, | 146 | http.Server, |
| 158 | unknown, // TODO: This should not be unknown | 147 | unknown, // TODO: This should not be unknown |
| 159 | Scope.Scope | ServerRuntimeServices | ServerConfig | 148 | Scope.Scope | ServerRuntimeServices | ServerConfig |
| 160 | > { | 149 | > { |
| 161 | const serverConfig = yield* ServerConfig; | 150 | const serverConfig = yield* ServerConfig; |
| 162 | const { port, cwd, staticDir, devUrl, authToken, mode, host } = serverConfig; | ||
| 163 | const { | 151 | const { |
| 164 | logWebSocketEvents = parseBooleanEnv(process.env.T3CODE_LOG_WS_EVENTS) ?? Boolean(devUrl), | 152 | port, |
| 165 | autoBootstrapProjectFromCwd = mode === "web", | 153 | cwd, |
| 166 | } = options; | 154 | staticDir, |
| 155 | devUrl, | ||
| 156 | authToken, | ||
| 157 | host, | ||
| 158 | logWebSocketEvents, | ||
| 159 | autoBootstrapProjectFromCwd, | ||
| 160 | } = serverConfig; | ||
| 167 | 161 | ||
| 168 | const gitManager = yield* GitManager; | 162 | const gitManager = yield* GitManager; |
| 169 | const terminalManager = yield* TerminalManager; | 163 | const terminalManager = yield* TerminalManager; |
| ... | @@ -599,6 +593,6 @@ export const createServer = Effect.fn(function* (options: ServerOptions = {}): E | ... | @@ -599,6 +593,6 @@ export const createServer = Effect.fn(function* (options: ServerOptions = {}): E |
| 599 | }); | 593 | }); |
| 600 | 594 | ||
| 601 | export const ServerLive = Layer.succeed(Server, { | 595 | export const ServerLive = Layer.succeed(Server, { |
| 602 | createServer, | 596 | start: createServer(), |
| 603 | stopSignal: Effect.never, | 597 | stopSignal: Effect.never, |
| 604 | } satisfies ServerShape); | 598 | } satisfies ServerShape); |