UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

257 lines (229 loc) 8.37 kB
import { $inject, Alepha, AlephaError } from "alepha"; import type { AppEntry } from "alepha/cli"; import type { RunnerMethod } from "alepha/command"; import { $logger, ConsoleColorProvider } from "alepha/logger"; import { CloudflareAdapter } from "../adapters/CloudflareAdapter.ts"; import type { DetectedResources, PlatformAdapter, PlatformContext, PlatformState, } from "../adapters/PlatformAdapter.ts"; import { VercelAdapter } from "../adapters/VercelAdapter.ts"; import { type NamingContext, NamingService, resolveTenant, tenantDomain, } from "./NamingService.ts"; import { PlatformInspector, type ResolvedPlatformConfig, } from "./PlatformInspector.ts"; /** * Orchestrates platform lifecycle operations. * * Coordinates adapter calls in the correct order for * up (build -> migrate -> deploy), down, plan, and status. */ export class PlatformOrchestrator { protected readonly log = $logger(); protected readonly color = $inject(ConsoleColorProvider); protected readonly inspector = $inject(PlatformInspector); protected readonly naming = $inject(NamingService); protected readonly cloudflareAdapter = $inject(CloudflareAdapter); protected readonly vercelAdapter = $inject(VercelAdapter); protected readonly alepha = $inject(Alepha); // ------------------------------------------------------------------------- // Adapter resolution // ------------------------------------------------------------------------- public resolveAdapter(adapterName: string): PlatformAdapter { switch (adapterName) { case "cloudflare": return this.cloudflareAdapter; case "vercel": return this.vercelAdapter; default: throw new AlephaError(`Unknown adapter: "${adapterName}"`); } } // ------------------------------------------------------------------------- // up // ------------------------------------------------------------------------- public async up(options: { root: string; env: string; entry: AppEntry; resources: DetectedResources; run: RunnerMethod; /** * Pre-built mode — the artifact's `dist/` is already produced. * * Still runs auth → provision → build → migrate → deploy → secrets, * but the `build` step shells out to `alepha build --prebuilt` which * only regenerates the target-specific deploy config (e.g. * `wrangler.jsonc`) and skips the Vite client + server builds. * Used by external orchestrators (Rocket) that ship a pre-built * `dist/` and just need the wrangler config refreshed for * per-tenant overrides on every deploy. */ prebuilt?: boolean; /** Tenant slug (apps with tenancy optional|required). */ tenant?: string; }): Promise<{ urls: string[]; domain?: string }> { const { root, env, entry, resources, run, prebuilt } = options; const envConfig = await this.inspector.resolveEnvironment(root, env); const config = await this.inspector.resolveConfig(root); const adapter = this.resolveAdapter(envConfig.adapter); const tenant = resolveTenant(config.tenancy, options.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx: PlatformContext = { project: config.project, env, envConfig, root, entry, resources, naming: namingCtx, tenant, prebuilt, }; await adapter.authenticate(ctx, run); await adapter.provision(ctx, run); // `build` always runs — adapter checks `ctx.prebuilt` to decide // whether to do a full bundle build or only regenerate deploy config. await adapter.build(ctx, run); await adapter.migrate(ctx, run); const url = await adapter.deploy(ctx, run); await adapter.secrets(ctx, run); run.end(); return { urls: url ? [url] : [], domain: tenantDomain(envConfig.domain, tenant), }; } /** * Pretty-print the `up()` result to stdout. Matches the formatting the * orchestrator used to emit inline; split out so callers that want * JSON output can skip this branch. */ public printUpSummary(result: { urls: string[]; domain?: string }): void { const c = this.color; if (result.domain) { this.log.info(""); const display = result.domain.includes("*") ? `https://${result.domain} (wildcard route)` : `https://${result.domain}`; this.log.info(` ${c.set("GREEN", "\u2192")} ${c.set("CYAN", display)}`); this.log.info(""); } else { for (const url of result.urls) { this.log.info(""); this.log.info(` ${c.set("GREEN", "\u2192")} ${c.set("CYAN", url)}`); this.log.info(""); } } } // ------------------------------------------------------------------------- // down // ------------------------------------------------------------------------- public async down(options: { root: string; env: string; entry: AppEntry; resources: DetectedResources; run: RunnerMethod; confirm: (prompt: string) => Promise<string>; /** Tenant slug (apps with tenancy optional|required). */ tenant?: string; }): Promise<boolean> { const { root, env, entry, resources, run, confirm } = options; const envConfig = await this.inspector.resolveEnvironment(root, env); const config = await this.inspector.resolveConfig(root); const adapter = this.resolveAdapter(envConfig.adapter); const tenant = resolveTenant(config.tenancy, options.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx: PlatformContext = { project: config.project, env, envConfig, root, entry, resources, naming: namingCtx, tenant, }; // Confirm (skip for tmp envs) if (!this.isTmpEnv(env)) { const answer = await confirm(`Type "${env}" to confirm teardown:`); if (answer !== env) { this.log.info("Aborted."); return false; } } // Auth await adapter.authenticate(ctx, run); // Teardown await adapter.teardown(ctx, run); run.end(); return true; } // ------------------------------------------------------------------------- // plan // ------------------------------------------------------------------------- public async plan(options: { root: string; env: string; resources: DetectedResources; /** Tenant slug (apps with tenancy optional|required). */ tenant?: string; }): Promise<{ config: ResolvedPlatformConfig; naming: NamingContext; resources: DetectedResources; }> { const { root, env, resources } = options; const config = await this.inspector.resolveConfig(root); const tenant = resolveTenant(config.tenancy, options.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); return { config, naming: namingCtx, resources }; } // ------------------------------------------------------------------------- // status // ------------------------------------------------------------------------- public async status(options: { root: string; env: string; entry: AppEntry; resources: DetectedResources; run: RunnerMethod; /** Tenant slug (apps with tenancy optional|required). */ tenant?: string; }): Promise<{ config: ResolvedPlatformConfig; state: PlatformState }> { const { root, env, entry, resources, run } = options; const envConfig = await this.inspector.resolveEnvironment(root, env); const config = await this.inspector.resolveConfig(root); const adapter = this.resolveAdapter(envConfig.adapter); const tenant = resolveTenant(config.tenancy, options.tenant); const namingCtx = this.naming.forContext(config.project, env, tenant); const ctx: PlatformContext = { project: config.project, env, envConfig, root, entry, resources, naming: namingCtx, tenant, }; await adapter.authenticate(ctx, run); const state = await adapter.inspect(ctx, run); return { config, state }; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- public isTmpEnv(env: string): boolean { return env.startsWith("tmp"); } }