277 lines · 9.8 KB · TypeScript
RawDownload
import type {  DesktopAppBranding,  DesktopAppStageLabel,  DesktopRuntimeArch,  DesktopRuntimeInfo,} from "@t3tools/contracts";import * as Config from "effect/Config";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";import * as DesktopConfig from "./DesktopConfig.ts";import { resolveLinuxDesktopEntryName } from "./DesktopEarlyElectronStartup.ts";import { resolveDesktopBaseDir, resolveDesktopStateDir } from "./DesktopStatePaths.ts";import { isNightlyDesktopVersion } from "../updates/updateChannels.ts"; export interface MakeDesktopEnvironmentInput {  readonly dirname: string;  readonly homeDirectory: string;  readonly platform: NodeJS.Platform;  readonly processArch: string;  readonly appVersion: string;  readonly appPath: string;  readonly isPackaged: boolean;  readonly resourcesPath: string;  readonly runningUnderArm64Translation: boolean;} export class DesktopEnvironment extends Context.Service<  DesktopEnvironment,  {    readonly path: Path.Path;    readonly dirname: string;    readonly platform: NodeJS.Platform;    readonly processArch: string;    readonly isPackaged: boolean;    readonly isDevelopment: boolean;    readonly appVersion: string;    readonly appPath: string;    readonly resourcesPath: string;    readonly homeDirectory: string;    readonly appDataDirectory: string;    readonly baseDir: string;    readonly stateDir: string;    readonly desktopSettingsPath: string;    readonly clientSettingsPath: string;    readonly savedEnvironmentRegistryPath: string;    readonly serverSettingsPath: string;    readonly logDir: string;    readonly browserArtifactsDir: string;    readonly rootDir: string;    readonly appRoot: string;    // Root of the tree containing apps/server/dist and node_modules for the    // backend. Equals appRoot everywhere except packaged Windows, where the    // server tree ships as the resources/server.asar sidecar (see    // scripts/build-desktop-artifact.ts) that the asar-aware    // ELECTRON_RUN_AS_NODE primary reads in place and the WSL backend    // extracts on demand (see DesktopWslServerTree).    readonly serverRoot: string;    readonly backendEntryPath: string;    readonly backendCwd: string;    readonly preloadPath: string;    readonly appUpdateYmlPath: string;    readonly devServerUrl: Option.Option<URL>;    readonly devRemoteT3ServerEntryPath: Option.Option<string>;    readonly configuredBackendPort: Option.Option<number>;    readonly commitHashOverride: Option.Option<string>;    readonly otlpTracesUrl: Option.Option<string>;    readonly otlpExportIntervalMs: number;    readonly branding: DesktopAppBranding;    readonly displayName: string;    readonly appUserModelId: string;    readonly linuxDesktopEntryName: string;    readonly linuxWmClass: string;    readonly linuxApplicationsDir: string;    readonly appImagePath: Option.Option<string>;    readonly userDataDirName: string;    readonly legacyUserDataDirName: string;    readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings;    readonly runtimeInfo: DesktopRuntimeInfo;    readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>;    readonly resolveResourcePathCandidates: (fileName: string) => readonly string[];  }>()("@t3tools/desktop/app/DesktopEnvironment") {} const APP_BASE_NAME = "T3 Code"; function resolveDesktopAppStageLabel(input: {  readonly isDevelopment: boolean;  readonly appVersion: string;}): DesktopAppStageLabel {  if (input.isDevelopment) {    return "Dev";  }   return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";} export function resolveDesktopAppBranding(input: {  readonly isDevelopment: boolean;  readonly appVersion: string;}): DesktopAppBranding {  const stageLabel = resolveDesktopAppStageLabel(input);  return {    baseName: APP_BASE_NAME,    stageLabel,    displayName: `${APP_BASE_NAME} (${stageLabel})`,  };} function normalizeDesktopArch(arch: string): DesktopRuntimeArch {  if (arch === "arm64") return "arm64";  if (arch === "x64") return "x64";  return "other";} function resolveDesktopRuntimeInfo(input: {  readonly platform: NodeJS.Platform;  readonly processArch: string;  readonly runningUnderArm64Translation: boolean;}): DesktopRuntimeInfo {  const appArch = normalizeDesktopArch(input.processArch);   if (input.platform !== "darwin") {    return {      hostArch: appArch,      appArch,      runningUnderArm64Translation: false,    };  }   const hostArch = appArch === "arm64" || input.runningUnderArm64Translation ? "arm64" : appArch;   return {    hostArch,    appArch,    runningUnderArm64Translation: input.runningUnderArm64Translation,  };} const make = Effect.fn("desktop.environment.make")(function* (  input: MakeDesktopEnvironmentInput,): Effect.fn.Return<DesktopEnvironment["Service"], Config.ConfigError, Path.Path> {  const path = yield* Path.Path;  const config = yield* DesktopConfig.DesktopConfig;  const homeDirectory = input.homeDirectory;  const devServerUrl = config.devServerUrl;  const isDevelopment = Option.isSome(devServerUrl);  const appDataDirectory =    input.platform === "win32"      ? Option.getOrElse(config.appDataDirectory, () =>          path.join(homeDirectory, "AppData", "Roaming"),        )      : input.platform === "darwin"        ? path.join(homeDirectory, "Library", "Application Support")        : Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config"));  const baseDir = resolveDesktopBaseDir({    homeDirectory,    joinPath: path.join,    t3Home: config.t3Home,  });  const rootDir = path.resolve(input.dirname, "../../..");  const appRoot = input.isPackaged ? input.appPath : rootDir;  const serverRoot =    input.isPackaged && input.platform === "win32"      ? path.join(input.resourcesPath, "server.asar")      : appRoot;  const branding = resolveDesktopAppBranding({    isDevelopment,    appVersion: input.appVersion,  });  const displayName = branding.displayName;  const stateDir = resolveDesktopStateDir({    baseDir,    isDevelopment,    joinPath: path.join,    t3Home: config.t3Home,  });  const userDataDirName = isDevelopment ? "t3code-dev" : "t3code";  const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";  const linuxApplicationsDir = path.join(    Option.getOrElse(config.xdgDataHome, () => path.join(homeDirectory, ".local", "share")),    "applications",  );  const resourcesPath = input.resourcesPath;   return DesktopEnvironment.of({    path,    dirname: input.dirname,    platform: input.platform,    processArch: input.processArch,    isPackaged: input.isPackaged,    isDevelopment,    appVersion: input.appVersion,    appPath: input.appPath,    resourcesPath,    homeDirectory,    appDataDirectory,    baseDir,    stateDir,    desktopSettingsPath: path.join(stateDir, "desktop-settings.json"),    clientSettingsPath: path.join(stateDir, "client-settings.json"),    savedEnvironmentRegistryPath: path.join(stateDir, "saved-environments.json"),    serverSettingsPath: path.join(stateDir, "settings.json"),    logDir: path.join(stateDir, "logs"),    browserArtifactsDir: path.join(stateDir, "browser-artifacts"),    rootDir,    appRoot,    serverRoot,    backendEntryPath: path.join(serverRoot, "apps/server/dist/bin.mjs"),    backendCwd: input.isPackaged ? homeDirectory : appRoot,    preloadPath: path.join(input.dirname, "preload.cjs"),    appUpdateYmlPath: input.isPackaged      ? path.join(resourcesPath, "app-update.yml")      : path.join(input.appPath, "dev-app-update.yml"),    devServerUrl,    devRemoteT3ServerEntryPath: config.devRemoteT3ServerEntryPath,    configuredBackendPort: config.configuredBackendPort,    commitHashOverride: config.commitHashOverride,    otlpTracesUrl: config.otlpTracesUrl,    otlpExportIntervalMs: config.otlpExportIntervalMs,    branding,    displayName,    appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () =>      isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code",    ),    linuxDesktopEntryName: resolveLinuxDesktopEntryName(isDevelopment),    linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",    linuxApplicationsDir,    appImagePath: config.appImagePath,    userDataDirName,    legacyUserDataDirName,    defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion),    runtimeInfo: resolveDesktopRuntimeInfo({      platform: input.platform,      processArch: input.processArch,      runningUnderArm64Translation: input.runningUnderArm64Translation,    }),    resolvePickFolderDefaultPath: (rawOptions) => {      if (typeof rawOptions !== "object" || rawOptions === null) {        return Option.none();      }       const { initialPath } = rawOptions as { initialPath?: unknown };      if (typeof initialPath !== "string") {        return Option.none();      }       const trimmedPath = initialPath.trim();      if (trimmedPath.length === 0) {        return Option.none();      }       if (trimmedPath === "~") {        return Option.some(homeDirectory);      }       if (trimmedPath.startsWith("~/") || trimmedPath.startsWith("~\\")) {        return Option.some(path.join(homeDirectory, trimmedPath.slice(2)));      }       return Option.some(path.resolve(trimmedPath));    },    resolveResourcePathCandidates: (fileName) => [      path.join(input.dirname, "../resources", fileName),      path.join(input.dirname, "../prod-resources", fileName),      path.join(resourcesPath, "resources", fileName),      path.join(resourcesPath, fileName),    ],  });}); export const layer = (input: MakeDesktopEnvironmentInput) =>  Layer.effect(DesktopEnvironment, make(input));