152 lines · 5.4 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema"; import * as ElectronApp from "../electron/ElectronApp.ts";import * as DesktopAssets from "./DesktopAssets.ts";import * as DesktopEnvironment from "./DesktopEnvironment.ts"; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;const COMMIT_HASH_DISPLAY_LENGTH = 12; const AppPackageMetadata = Schema.Struct({  t3codeCommitHash: Schema.optional(Schema.String),});const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata)); export class DesktopUserDataPathResolutionError extends Schema.TaggedError<DesktopUserDataPathResolutionError>()(  "DesktopUserDataPathResolutionError",  {    legacyPath: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to inspect legacy desktop user-data path at "${this.legacyPath}".`;  }} export class DesktopAppIdentity extends Context.Service<  DesktopAppIdentity,  {    readonly resolveUserDataPath: Effect.Effect<string, DesktopUserDataPathResolutionError>;    readonly configure: Effect.Effect<void>;  }>()("@t3tools/desktop/app/DesktopAppIdentity") {} const normalizeCommitHash = (value: string): Option.Option<string> => {  const trimmed = value.trim();  return COMMIT_HASH_PATTERN.test(trimmed)    ? Option.some(trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase())    : Option.none();}; export const resolveUserDataPath = Effect.gen(function* () {  const environment = yield* DesktopEnvironment.DesktopEnvironment;  const fileSystem = yield* FileSystem.FileSystem;  const legacyPath = environment.path.join(    environment.appDataDirectory,    environment.legacyUserDataDirName,  );  const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(    Effect.mapError(      (cause) =>        new DesktopUserDataPathResolutionError({          legacyPath,          cause,        }),    ),  );  return legacyPathExists    ? legacyPath    : environment.path.join(environment.appDataDirectory, environment.userDataDirName);}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath")); /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.gen(function* () {  const assets = yield* DesktopAssets.DesktopAssets;  const electronApp = yield* ElectronApp.ElectronApp;  const environment = yield* DesktopEnvironment.DesktopEnvironment;  const fileSystem = yield* FileSystem.FileSystem;  const commitHashCache = yield* Ref.make<Option.Option<Option.Option<string>>>(Option.none());   const resolveEmbeddedCommitHash = Effect.gen(function* () {    const packageJsonPath = environment.path.join(environment.appRoot, "package.json");    const raw = yield* fileSystem.readFileString(packageJsonPath).pipe(Effect.option);    return yield* Option.match(raw, {      onNone: () => Effect.succeed(Option.none<string>()),      onSome: (value) =>        decodeAppPackageMetadata(value).pipe(          Effect.map((parsed) =>            Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),          ),          Effect.orElseSucceed(() => Option.none<string>()),        ),    });  });   const resolveAboutCommitHash = Effect.gen(function* () {    const cached = yield* Ref.get(commitHashCache);    if (Option.isSome(cached)) {      return cached.value;    }     const override = Option.flatMap(environment.commitHashOverride, normalizeCommitHash);    if (Option.isSome(override)) {      yield* Ref.set(commitHashCache, Option.some(override));      return override;    }     if (!environment.isPackaged) {      const empty = Option.none<string>();      yield* Ref.set(commitHashCache, Option.some(empty));      return empty;    }     const commitHash = yield* resolveEmbeddedCommitHash;    yield* Ref.set(commitHashCache, Option.some(commitHash));    return commitHash;  });   const userDataPath = resolveUserDataPath.pipe(    Effect.provide(      yield* Effect.context<DesktopEnvironment.DesktopEnvironment | FileSystem.FileSystem>(),    ),  );   const configure = Effect.gen(function* () {    const commitHash = yield* resolveAboutCommitHash;    yield* electronApp.setName(environment.displayName);    yield* electronApp.setAboutPanelOptions({      applicationName: environment.displayName,      applicationVersion: environment.appVersion,      version: Option.getOrElse(commitHash, () => "unknown"),    });     if (environment.platform === "win32") {      yield* electronApp.setAppUserModelId(environment.appUserModelId);    }     // Unpackaged runs only. A packaged bundle already carries its icon in    // Info.plist, so setting the dock tile again changes nothing except to    // overwrite a custom icon the user attached to the app themselves.    if (environment.platform === "darwin" && !environment.isPackaged) {      const iconPaths = yield* assets.iconPaths;      yield* Option.match(iconPaths.png, {        onNone: () => Effect.void,        onSome: electronApp.setDockIcon,      });    }  }).pipe(Effect.withSpan("desktop.appIdentity.configure"));   return DesktopAppIdentity.of({    resolveUserDataPath: userDataPath,    configure,  });}); export const layer = Layer.effect(DesktopAppIdentity, make);