649 lines · 20.1 KB · TypeScript
RawDownload
#!/usr/bin/env node import * as NodeOS from "node:os"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import * as NetService from "@t3tools/shared/Net";import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";import { resolveSpawnCommand } from "@t3tools/shared/shell";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as Hash from "effect/Hash";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import { Argument, Command, Flag } from "effect/unstable/cli";import { ChildProcess } from "effect/unstable/process"; import { loadRepoEnv } from "./lib/public-config.ts"; Object.assign(process.env, loadRepoEnv()); const BASE_SERVER_PORT = 13773;const BASE_WEB_PORT = 5733;const MAX_HASH_OFFSET = 3000;const MAX_PORT = 65535;const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const; export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) =>  path.join(NodeOS.homedir(), ".t3"),); const MODE_ARGS = {  dev: [    "run",    "--filter=@t3tools/contracts",    "--filter=@t3tools/web",    "--filter=t3",    "--parallel",    "dev",  ],  "dev:server": ["run", "--filter=t3", "dev"],  "dev:web": ["run", "--filter=@t3tools/web", "dev"],  "dev:desktop": ["run", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "dev"],} as const satisfies Record<string, ReadonlyArray<string>>; type DevMode = keyof typeof MODE_ARGS;type PortAvailabilityCheck<R = never> = (port: number) => Effect.Effect<boolean, never, R>; const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array<DevMode>; export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {  return MODE_ARGS[mode];} export class DevRunnerConfigurationError extends Schema.TaggedErrorClass<DevRunnerConfigurationError>()(  "DevRunnerConfigurationError",  {    configKeys: Schema.Array(Schema.String),    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to read dev-runner configuration: ${this.configKeys.join(", ")}.`;  }} export class DevRunnerInvalidPortOffsetError extends Schema.TaggedErrorClass<DevRunnerInvalidPortOffsetError>()(  "DevRunnerInvalidPortOffsetError",  {    configKey: Schema.Literal("T3CODE_PORT_OFFSET"),    portOffset: Schema.Number,    minimum: Schema.Number,  },) {  override get message(): string {    return `${this.configKey} must be at least ${this.minimum}; received ${this.portOffset}.`;  }} export class DevRunnerPortExhaustedError extends Schema.TaggedErrorClass<DevRunnerPortExhaustedError>()(  "DevRunnerPortExhaustedError",  {    startOffset: Schema.Number,    requireServerPort: Schema.Boolean,    requireWebPort: Schema.Boolean,    baseServerPort: Schema.Number,    baseWebPort: Schema.Number,    maximumPort: Schema.Number,  },) {  override get message(): string {    return `No required dev ports were available from offset ${this.startOffset} through maximum port ${this.maximumPort}.`;  }} export class DevRunnerProcessError extends Schema.TaggedErrorClass<DevRunnerProcessError>()(  "DevRunnerProcessError",  {    operation: Schema.Literals(["spawn", "wait-for-exit"]),    mode: Schema.Literals(["dev", "dev:server", "dev:web", "dev:desktop"]),    executable: Schema.Literal("vp"),    argumentCount: Schema.Number,    shell: Schema.Boolean,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Dev-runner process operation "${this.operation}" failed for mode "${this.mode}".`;  }} export class DevRunnerProcessExitError extends Schema.TaggedErrorClass<DevRunnerProcessExitError>()(  "DevRunnerProcessExitError",  {    mode: Schema.Literals(["dev", "dev:server", "dev:web", "dev:desktop"]),    executable: Schema.Literal("vp"),    argumentCount: Schema.Number,    shell: Schema.Boolean,    exitCode: Schema.Number,  },) {  override get message(): string {    return `Dev-runner process exited with code ${this.exitCode} in mode "${this.mode}".`;  }} export const DevRunnerError = Schema.Union([  DevRunnerConfigurationError,  DevRunnerInvalidPortOffsetError,  DevRunnerPortExhaustedError,  DevRunnerProcessError,  DevRunnerProcessExitError,]);export type DevRunnerError = typeof DevRunnerError.Type;export const isDevRunnerError = Schema.is(DevRunnerError); const optionalStringConfig = (name: string): Config.Config<string | undefined> =>  Config.string(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalBooleanConfig = (name: string): Config.Config<boolean | undefined> =>  Config.boolean(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalPortConfig = (name: string): Config.Config<number | undefined> =>  Config.port(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalIntegerConfig = (name: string): Config.Config<number | undefined> =>  Config.int(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const OffsetConfig = Config.all({  portOffset: optionalIntegerConfig("T3CODE_PORT_OFFSET"),  devInstance: optionalStringConfig("T3CODE_DEV_INSTANCE"),}); export function resolveOffset(config: {  readonly portOffset: number | undefined;  readonly devInstance: string | undefined;}): Effect.Effect<  { readonly offset: number; readonly source: string },  DevRunnerInvalidPortOffsetError> {  if (config.portOffset !== undefined) {    if (config.portOffset < 0) {      return Effect.fail(        new DevRunnerInvalidPortOffsetError({          configKey: "T3CODE_PORT_OFFSET",          portOffset: config.portOffset,          minimum: 0,        }),      );    }    return Effect.succeed({      offset: config.portOffset,      source: `T3CODE_PORT_OFFSET=${config.portOffset}`,    });  }   const seed = config.devInstance?.trim();  if (!seed) {    return Effect.succeed({ offset: 0, source: "default ports" });  }   if (/^\d+$/.test(seed)) {    return Effect.succeed({      offset: Number(seed),      source: `numeric T3CODE_DEV_INSTANCE=${seed}`,    });  }   const offset = ((Hash.string(seed) >>> 0) % MAX_HASH_OFFSET) + 1;  return Effect.succeed({ offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` });} function resolveBaseDir(baseDir: string | undefined): Effect.Effect<string, never, Path.Path> {  return Effect.gen(function* () {    const path = yield* Path.Path;    const configured = baseDir?.trim();     if (configured) {      return path.resolve(configured);    }     return yield* DEFAULT_T3_HOME;  });} interface CreateDevRunnerEnvInput {  readonly mode: DevMode;  readonly baseEnv: NodeJS.ProcessEnv;  readonly serverOffset: number;  readonly webOffset: number;  readonly t3Home: string | undefined;  readonly noBrowser: boolean | undefined;  readonly autoBootstrapProjectFromCwd: boolean | undefined;  readonly logWebSocketEvents: boolean | undefined;  readonly host: string | undefined;  readonly port: number | undefined;  readonly devUrl: URL | undefined;} export function createDevRunnerEnv({  mode,  baseEnv,  serverOffset,  webOffset,  t3Home,  noBrowser,  autoBootstrapProjectFromCwd,  logWebSocketEvents,  host,  port,  devUrl,}: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, never, Path.Path> {  return Effect.gen(function* () {    const serverPort = port ?? BASE_SERVER_PORT + serverOffset;    const webPort = BASE_WEB_PORT + webOffset;    const resolvedBaseDir = yield* resolveBaseDir(t3Home);    const isDesktopMode = mode === "dev:desktop";     const output: NodeJS.ProcessEnv = {      ...baseEnv,      PORT: String(webPort),      VITE_DEV_SERVER_URL:        devUrl?.toString() ??        `http://${isDesktopMode ? DESKTOP_DEV_LOOPBACK_HOST : "localhost"}:${webPort}`,      T3CODE_HOME: resolvedBaseDir,    };     if (!isDesktopMode) {      output.T3CODE_PORT = String(serverPort);      output.VITE_HTTP_URL = `http://localhost:${serverPort}`;      output.VITE_WS_URL = `ws://localhost:${serverPort}`;    } else {      output.T3CODE_PORT = String(serverPort);      output.VITE_HTTP_URL = `http://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`;      output.VITE_WS_URL = `ws://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`;      delete output.T3CODE_MODE;      delete output.T3CODE_NO_BROWSER;      delete output.T3CODE_HOST;    }     if (!isDesktopMode && host !== undefined) {      output.T3CODE_HOST = host;    }     if (!isDesktopMode && noBrowser !== undefined) {      output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0";    } else if (!isDesktopMode) {      delete output.T3CODE_NO_BROWSER;    }     if (autoBootstrapProjectFromCwd !== undefined) {      output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0";    } else {      delete output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD;    }     if (logWebSocketEvents !== undefined) {      output.T3CODE_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0";    } else {      delete output.T3CODE_LOG_WS_EVENTS;    }     if (mode === "dev") {      output.T3CODE_MODE = "web";      delete output.T3CODE_DESKTOP_WS_URL;    }     if (mode === "dev:server" || mode === "dev:web") {      output.T3CODE_MODE = "web";      delete output.T3CODE_DESKTOP_WS_URL;    }     if (isDesktopMode) {      output.HOST = DESKTOP_DEV_LOOPBACK_HOST;      delete output.T3CODE_DESKTOP_WS_URL;    }     return output;  });} function portPairForOffset(offset: number): {  readonly serverPort: number;  readonly webPort: number;} {  return {    serverPort: BASE_SERVER_PORT + offset,    webPort: BASE_WEB_PORT + offset,  };} export function checkPortAvailabilityOnHosts<R>(  port: number,  hosts: ReadonlyArray<string>,  canListenOnHost: (port: number, host: string) => Effect.Effect<boolean, never, R>,): Effect.Effect<boolean, never, R> {  return Effect.gen(function* () {    for (const host of hosts) {      if (!(yield* canListenOnHost(port, host))) {        return false;      }    }     return true;  });} const defaultCheckPortAvailability: PortAvailabilityCheck<NetService.NetService> = (port) =>  Effect.gen(function* () {    const net = yield* NetService.NetService;    return yield* checkPortAvailabilityOnHosts(port, DEV_PORT_PROBE_HOSTS, (candidatePort, host) =>      net.canListenOnHost(candidatePort, host),    );  }); interface FindFirstAvailableOffsetInput<R = NetService.NetService> {  readonly startOffset: number;  readonly requireServerPort: boolean;  readonly requireWebPort: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function findFirstAvailableOffset<R = NetService.NetService>({  startOffset,  requireServerPort,  requireWebPort,  checkPortAvailability,}: FindFirstAvailableOffsetInput<R>): Effect.Effect<number, DevRunnerPortExhaustedError, R> {  return Effect.gen(function* () {    const checkPort = (checkPortAvailability ??      defaultCheckPortAvailability) as PortAvailabilityCheck<R>;     for (let candidate = startOffset; ; candidate += 1) {      const { serverPort, webPort } = portPairForOffset(candidate);      const serverPortOutOfRange = serverPort > MAX_PORT;      const webPortOutOfRange = webPort > MAX_PORT;       if (        (requireServerPort && serverPortOutOfRange) ||        (requireWebPort && webPortOutOfRange) ||        (!requireServerPort && !requireWebPort && (serverPortOutOfRange || webPortOutOfRange))      ) {        break;      }       const checks: Array<Effect.Effect<boolean, never, R>> = [];      if (requireServerPort) {        checks.push(checkPort(serverPort));      }      if (requireWebPort) {        checks.push(checkPort(webPort));      }       if (checks.length === 0) {        return candidate;      }       const availability = yield* Effect.all(checks);      if (availability.every(Boolean)) {        return candidate;      }    }     return yield* new DevRunnerPortExhaustedError({      startOffset,      requireServerPort,      requireWebPort,      baseServerPort: BASE_SERVER_PORT,      baseWebPort: BASE_WEB_PORT,      maximumPort: MAX_PORT,    });  });} interface ResolveModePortOffsetsInput<R = NetService.NetService> {  readonly mode: DevMode;  readonly startOffset: number;  readonly hasExplicitServerPort: boolean;  readonly hasExplicitDevUrl: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function resolveModePortOffsets<R = NetService.NetService>({  mode,  startOffset,  hasExplicitServerPort,  hasExplicitDevUrl,  checkPortAvailability,}: ResolveModePortOffsetsInput<R>): Effect.Effect<  { readonly serverOffset: number; readonly webOffset: number },  DevRunnerPortExhaustedError,  R> {  return Effect.gen(function* () {    const checkPort = (checkPortAvailability ??      defaultCheckPortAvailability) as PortAvailabilityCheck<R>;     if (mode === "dev:web") {      if (hasExplicitDevUrl) {        return { serverOffset: startOffset, webOffset: startOffset };      }       const webOffset = yield* findFirstAvailableOffset({        startOffset,        requireServerPort: false,        requireWebPort: true,        checkPortAvailability: checkPort,      });      return { serverOffset: startOffset, webOffset };    }     if (mode === "dev:server") {      if (hasExplicitServerPort) {        return { serverOffset: startOffset, webOffset: startOffset };      }       const serverOffset = yield* findFirstAvailableOffset({        startOffset,        requireServerPort: true,        requireWebPort: false,        checkPortAvailability: checkPort,      });      return { serverOffset, webOffset: serverOffset };    }     const sharedOffset = yield* findFirstAvailableOffset({      startOffset,      requireServerPort: !hasExplicitServerPort,      requireWebPort: !hasExplicitDevUrl,      checkPortAvailability: checkPort,    });     return { serverOffset: sharedOffset, webOffset: sharedOffset };  });} interface DevRunnerCliInput {  readonly mode: DevMode;  readonly t3Home: string | undefined;  readonly noBrowser: boolean | undefined;  readonly autoBootstrapProjectFromCwd: boolean | undefined;  readonly logWebSocketEvents: boolean | undefined;  readonly host: string | undefined;  readonly port: number | undefined;  readonly devUrl: URL | undefined;  readonly dryRun: boolean;  readonly runArgs: ReadonlyArray<string>;} export function runDevRunnerWithInput(input: DevRunnerCliInput) {  return Effect.gen(function* () {    const { portOffset, devInstance } = yield* OffsetConfig.pipe(      Effect.mapError(        (cause) =>          new DevRunnerConfigurationError({            configKeys: ["T3CODE_PORT_OFFSET", "T3CODE_DEV_INSTANCE"],            cause,          }),      ),    );     const { offset, source } = yield* resolveOffset({ portOffset, devInstance });     const { serverOffset, webOffset } = yield* resolveModePortOffsets({      mode: input.mode,      startOffset: offset,      hasExplicitServerPort: input.port !== undefined,      hasExplicitDevUrl: input.devUrl !== undefined,

Lines 1–500 of 649.