1785 lines · 58.4 KB · TypeScript
RawDownload
#!/usr/bin/env node import * as NodeModule from "node:module"; import { fromYaml } from "@t3tools/shared/schemaYaml";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";import { resolveSpawnCommand } from "@t3tools/shared/shell";import rootPackageJson from "../package.json" with { type: "json" };import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };import serverPackageJson from "../apps/server/package.json" with { type: "json" }; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";import { getDefaultBuildArch } from "./lib/build-target-arch.ts";import { loadRepoEnv } from "./lib/public-config.ts";import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";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 * as Stream from "effect/Stream";import { Command, Flag } from "effect/unstable/cli";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const LINUX_ICON_SIZES = [16, 22, 24, 32, 48, 64, 128, 256, 512] as const;const DESKTOP_APP_ID = "com.t3tools.t3code";const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]);const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); const WorkspaceConfig = Schema.Struct({  catalog: Schema.optional(Schema.Record(Schema.String, Schema.String)),  overrides: Schema.optional(Schema.Record(Schema.String, Schema.String)),  patchedDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),});type WorkspaceConfig = typeof WorkspaceConfig.Type; const StageWorkspaceConfig = Schema.Struct({  supportedArchitectures: Schema.Struct({    os: Schema.Array(Schema.String),    cpu: Schema.Array(Schema.String),  }),}); const RepoRoot = Effect.service(Path.Path).pipe(  Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),);const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);const decodeWorkspaceConfig = Schema.decodeEffect(fromYaml(WorkspaceConfig));const encodeStageWorkspaceConfig = Schema.encodeEffect(fromYaml(StageWorkspaceConfig)); const readWorkspaceConfig = Effect.fn("readWorkspaceConfig")(function* () {  const fs = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const repoRoot = yield* RepoRoot;  const workspaceYaml = yield* fs.readFileString(path.join(repoRoot, "pnpm-workspace.yaml"));  return yield* decodeWorkspaceConfig(workspaceYaml);}); interface DesktopBuildIconAssets {  readonly macIconPng: string;  readonly linuxIconPng: string;  readonly windowsIconIco: string;} interface PlatformConfig {  readonly cliFlag: "--mac" | "--linux" | "--win";  readonly defaultTarget: string;  readonly archChoices: ReadonlyArray<typeof BuildArch.Type>;} const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = {  mac: {    cliFlag: "--mac",    defaultTarget: "dmg",    archChoices: ["arm64", "x64", "universal"],  },  linux: {    cliFlag: "--linux",    defaultTarget: "AppImage",    archChoices: ["x64", "arm64"],  },  win: {    cliFlag: "--win",    defaultTarget: "nsis",    archChoices: ["x64", "arm64"],  },}; interface BuildCliInput {  readonly platform: Option.Option<typeof BuildPlatform.Type>;  readonly target: Option.Option<string>;  readonly arch: Option.Option<typeof BuildArch.Type>;  readonly buildVersion: Option.Option<string>;  readonly outputDir: Option.Option<string>;  readonly skipBuild: Option.Option<boolean>;  readonly keepStage: Option.Option<boolean>;  readonly signed: Option.Option<boolean>;  readonly verbose: Option.Option<boolean>;  readonly mockUpdates: Option.Option<boolean>;  readonly mockUpdateServerPort: Option.Option<number>;} function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined {  if (hostPlatform === "darwin") return "mac";  if (hostPlatform === "linux") return "linux";  if (hostPlatform === "win32") return "win";  return undefined;} const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof BuildPlatform.Type) {  const config = PLATFORM_CONFIG[platform];  if (!config) {    return "x64";  }   return yield* getDefaultBuildArch(platform, config);}); export class MacPasskeySigningConfigurationResolutionError extends Schema.TaggedErrorClass<MacPasskeySigningConfigurationResolutionError>()(  "MacPasskeySigningConfigurationResolutionError",  {    cause: Schema.Defect(),  },) {  static fromCause(    cause: unknown,  ): MacPasskeySigningConfigurationError | MacPasskeySigningConfigurationResolutionError {    return isMacPasskeySigningConfigurationError(cause)      ? cause      : new MacPasskeySigningConfigurationResolutionError({ cause });  }   override get message(): string {    return "Failed to resolve macOS passkey signing configuration.";  }} export class ClerkPasskeyNativePackageMissingError extends Schema.TaggedErrorClass<ClerkPasskeyNativePackageMissingError>()(  "ClerkPasskeyNativePackageMissingError",  {    packageName: Schema.String,    binaryFileName: Schema.String,    packageEntryPath: Schema.String,    platform: BuildPlatform,    arch: BuildArch,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Clerk passkey native package is missing: ${this.packageName}`;  }} export class UnsupportedHostBuildPlatformError extends Schema.TaggedErrorClass<UnsupportedHostBuildPlatformError>()(  "UnsupportedHostBuildPlatformError",  {    hostPlatform: Schema.String,  },) {  override get message(): string {    return `Unsupported host platform '${this.hostPlatform}'.`;  }} const InvalidMockUpdateServerPortReason = Schema.Literals([  "not-numeric",  "not-integer",  "out-of-range",]); export class InvalidMockUpdateServerPortError extends Schema.TaggedErrorClass<InvalidMockUpdateServerPortError>()(  "InvalidMockUpdateServerPortError",  {    reason: InvalidMockUpdateServerPortReason,    inputLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),    cause: Schema.Defect(),  },) {  override get message(): string {    return "Invalid mock update server port.";  }   static fromConfigValue(configuredPort: string, cause: unknown) {    return new InvalidMockUpdateServerPortError({      reason: invalidMockUpdateServerPortReason(configuredPort),      inputLength: configuredPort.length,      cause,    });  }} export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildCommandFailedError>()(  "BuildCommandFailedError",  {    command: Schema.String,    exitCode: Schema.Int,    stdoutTail: Schema.optionalKey(Schema.String),    stderrTail: Schema.optionalKey(Schema.String),  },) {  override get message(): string {    const outputSections = [      `Command: ${this.command}`,      formatOutputSection("stdout", this.stdoutTail ?? ""),      formatOutputSection("stderr", this.stderrTail ?? ""),    ].filter((section): section is string => section !== undefined);    const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : "";    return `Command exited with non-zero exit code (${this.exitCode})${outputSuffix}`;  }} const desktopIconPlatformNames = {  mac: "macOS",  linux: "Linux",  win: "Windows",} satisfies Record<typeof BuildPlatform.Type, string>; export class DesktopIconSourceMissingError extends Schema.TaggedErrorClass<DesktopIconSourceMissingError>()(  "DesktopIconSourceMissingError",  {    platform: BuildPlatform,    sourcePath: Schema.String,  },) {  override get message(): string {    return `Desktop ${desktopIconPlatformNames[this.platform]} icon source is missing at ${this.sourcePath}`;  }} export class BundledClientAssetsMissingError extends Schema.TaggedErrorClass<BundledClientAssetsMissingError>()(  "BundledClientAssetsMissingError",  {    indexPath: Schema.String,    missingFiles: Schema.Array(Schema.String),  },) {  override get message(): string {    const preview = this.missingFiles.slice(0, 6).join(", ");    const suffix = this.missingFiles.length > 6 ? ` (+${this.missingFiles.length - 6} more)` : "";    return `Bundled client references missing files in ${this.indexPath}: ${preview}${suffix}. Rebuild web/server artifacts.`;  }} export class UnsupportedDesktopBuildPlatformError extends Schema.TaggedErrorClass<UnsupportedDesktopBuildPlatformError>()(  "UnsupportedDesktopBuildPlatformError",  {    platform: Schema.String,  },) {  override get message(): string {    return `Unsupported platform '${this.platform}'.`;  }} const dependencyResolutionDescriptions = {  "server-production": "production dependencies",  "workspace-overrides": "overrides",  "desktop-runtime": "desktop runtime dependencies",} as const;const DependencyResolutionKind = Schema.Literals([  "server-production",  "workspace-overrides",  "desktop-runtime",]); export class DesktopBuildDependencyResolutionError extends Schema.TaggedErrorClass<DesktopBuildDependencyResolutionError>()(  "DesktopBuildDependencyResolutionError",  {    kind: DependencyResolutionKind,    manifestPath: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Could not resolve ${dependencyResolutionDescriptions[this.kind]} from ${this.manifestPath}.`;  }} export class MissingServerProductionDependenciesError extends Schema.TaggedErrorClass<MissingServerProductionDependenciesError>()(  "MissingServerProductionDependenciesError",  {    manifestPath: Schema.String,  },) {  override get message(): string {    return `Could not resolve production dependencies from ${this.manifestPath}.`;  }} const DesktopBuildInputArtifact = Schema.Literals([  "desktop-dist",  "desktop-resources",  "server-dist",  "bundled-server-client",]);type DesktopBuildInputArtifact = typeof DesktopBuildInputArtifact.Type;const desktopBuildInputArtifactNames = {  "desktop-dist": "desktopDist",  "desktop-resources": "desktopResources",  "server-dist": "serverDist",  "bundled-server-client": "bundled server client",} satisfies Record<DesktopBuildInputArtifact, string>; export class MissingDesktopBuildInputError extends Schema.TaggedErrorClass<MissingDesktopBuildInputError>()(  "MissingDesktopBuildInputError",  {    artifact: DesktopBuildInputArtifact,    artifactPath: Schema.String,    buildCommand: Schema.Literal("vp run build:desktop"),  },) {  override get message(): string {    return `Missing ${desktopBuildInputArtifactNames[this.artifact]} at ${this.artifactPath}. Run '${this.buildCommand}' first.`;  }} export class MacProvisioningProfileNotFoundError extends Schema.TaggedErrorClass<MacProvisioningProfileNotFoundError>()(  "MacProvisioningProfileNotFoundError",  {    provisioningProfilePath: Schema.String,  },) {  override get message(): string {    return `macOS provisioning profile not found: ${this.provisioningProfilePath}`;  }} export class DesktopBuildDistDirectoryMissingError extends Schema.TaggedErrorClass<DesktopBuildDistDirectoryMissingError>()(  "DesktopBuildDistDirectoryMissingError",  {    distPath: Schema.String,    platform: BuildPlatform,    arch: BuildArch,  },) {  override get message(): string {    return `Build completed but dist directory was not found at ${this.distPath}`;  }} export class DesktopBuildNoArtifactsProducedError extends Schema.TaggedErrorClass<DesktopBuildNoArtifactsProducedError>()(  "DesktopBuildNoArtifactsProducedError",  {    distPath: Schema.String,    platform: BuildPlatform,    arch: BuildArch,  },) {  override get message(): string {    return `Build completed but no files were produced in ${this.distPath}`;  }} export class LinuxIconResizeError extends Schema.TaggedErrorClass<LinuxIconResizeError>()(  "LinuxIconResizeError",  {    operation: Schema.Literal("resize"),    iconSize: Schema.Int,    primaryTool: Schema.Literal("magick"),    fallbackTool: Schema.Literal("convert"),    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to ${this.operation} the Linux desktop icon to ${this.iconSize}x${this.iconSize} with \`${this.primaryTool}\` or \`${this.fallbackTool}\`. Install ImageMagick so either tool is available.`;  }} const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>  stream.pipe(    Stream.decodeText(),    Stream.runFold(      () => "",      (acc, chunk) => acc + chunk,    ),  ); const COMMAND_OUTPUT_TAIL_LENGTH = 20_000; function appendOutputTail(acc: string, chunk: string): string {  const next = acc + chunk;  return next.length > COMMAND_OUTPUT_TAIL_LENGTH ? next.slice(-COMMAND_OUTPUT_TAIL_LENGTH) : next;} function formatOutputSection(label: string, output: string): string | undefined {  const trimmed = output.trim();  if (!trimmed) return undefined;  return `${label} tail:\n${trimmed}`;} const collectCommandStream = <E>(  stream: Stream.Stream<Uint8Array, E>,  output: NodeJS.WriteStream,  verbose: boolean,): Effect.Effect<string, E> =>  stream.pipe(    Stream.decodeText(),    Stream.runFoldEffect(      () => "",      (acc, chunk) =>        Effect.as(          verbose ? Effect.sync(() => output.write(chunk)) : Effect.void,          appendOutputTail(acc, chunk),        ),    ),  ); const spawnAndCollectOutput = Effect.fn("spawnAndCollectOutput")(function* (  command: ChildProcess.Command,) {  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;  const child = yield* spawner.spawn(command);   const [stdout, stderr, exitCode] = yield* Effect.all(    [      collectStreamAsString(child.stdout),      collectStreamAsString(child.stderr),      child.exitCode.pipe(Effect.map(Number)),    ],    { concurrency: "unbounded" },  );   return { stdout, stderr, exitCode } as const;}); const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRoot: string) {  const result = yield* spawnAndCollectOutput(    ChildProcess.make("git", ["rev-parse", "--short=12", "HEAD"], {      cwd: repoRoot,    }),  ).pipe(    Effect.orElseSucceed(() => ({      stdout: "",      stderr: "",      exitCode: 1,    })),  );   if (result.exitCode !== 0) {    return "unknown";  }  const hash = result.stdout.trim();  if (!/^[0-9a-f]{7,40}$/i.test(hash)) {    return "unknown";  }  return hash.toLowerCase();}); const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () {  const fs = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const hostPlatform = yield* HostProcessPlatform;  const env = yield* Config.all({    configuredPython: Config.string("npm_config_python").pipe(      Config.orElse(() => Config.string("PYTHON")),      Config.option,    ),    localAppData: Config.string("LOCALAPPDATA").pipe(Config.option),  });  const configured = Option.getOrUndefined(env.configuredPython);  if (configured && (yield* fs.exists(configured))) {    return configured;  }   if (hostPlatform === "win32") {    const localAppData = Option.getOrUndefined(env.localAppData);    if (localAppData) {      for (const version of ["Python313", "Python312", "Python311", "Python310"]) {        const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe");        if (yield* fs.exists(candidate)) {          return candidate;        }      }    }  }   const probe = yield* spawnAndCollectOutput(    ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]),  ).pipe(    Effect.orElseSucceed(() => ({      stdout: "",      stderr: "",      exitCode: 1,    })),  );   if (probe.exitCode !== 0) {    return undefined;  }   const executable = probe.stdout.trim();

Lines 1–500 of 1785.