< Summary - Envilder IaC (CDK)

Information
Class: src/iac/bin/main.ts
Assembly: Default
File(s): src/iac/bin/main.ts
Tag: 558_41035824231
Line coverage
87%
Covered lines: 71
Uncovered lines: 10
Coverable lines: 81
Total lines: 301
Line coverage: 87.6%
Branch coverage
75%
Covered branches: 45
Total branches: 60
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/iac/bin/main.ts

#LineLine coverage
 1#!/usr/bin/env node
 2import path from "node:path";
 3import type { Environment, Stack } from "aws-cdk-lib";
 4/**
 5 * CDK Infrastructure Deployment Entry Point
 6 */
 7import { App } from "aws-cdk-lib";
 8import { AppEnvironment } from "../lib/core/types";
 9import { PublicFilesStack } from "../lib/stacks/publicFilesStack";
 10import { StaticWebsiteStack } from "../lib/stacks/staticWebsiteStack";
 11
 12// ============================================================================
 13// Types
 14// ============================================================================
 15
 16interface StaticWebsiteConfig {
 17  name: string;
 18  projectPath: string;
 19  subdomain?: string;
 20}
 21
 22interface PublicFilesConfig {
 23  name: string;
 24  subdomain: string;
 25  certificateId: string;
 26}
 27
 28interface DeploymentConfig {
 29  repoName: string;
 30  branch: string;
 31  environment: AppEnvironment;
 32  domain: {
 33    name: string;
 34    certificateId: string;
 35    hostedZoneId: string;
 36  };
 37  stacks: {
 38    frontend: {
 39      staticWebsites: readonly StaticWebsiteConfig[];
 40    };
 41    publicFiles?: PublicFilesConfig;
 42  };
 43  rootPath?: string;
 44}
 45
 46// ============================================================================
 47// Configuration
 48// ============================================================================
 49
 150const config: DeploymentConfig = {
 51  repoName: "envilder",
 52  branch: "main",
 53  environment: AppEnvironment.Production,
 54  domain: {
 55    name: "envilder.com",
 56    certificateId: "e04983fe-1561-4ebe-9166-83f77789964a",
 57    hostedZoneId: "Z0718467FEEOZ35UNCTO",
 58  },
 59  stacks: {
 60    frontend: {
 61      staticWebsites: [
 62        {
 63          name: "Website",
 64          projectPath: "src/website/dist",
 65        },
 66      ],
 67    },
 68    publicFiles: {
 69      name: "Files",
 70      subdomain: "files",
 71      // *.envilder.com
 72      certificateId: "b5de0685-2e52-4ceb-9cb2-d66b164a791e",
 73    },
 74  },
 75};
 76
 77// ============================================================================
 78// Utils
 79// ============================================================================
 80
 81function getRootPath(rootPath?: string): string {
 582  return rootPath ?? path.resolve(__dirname, "../../..");
 83}
 84
 85function resolveFullPath(rootPath: string, relativePath: string): string {
 486  return path.join(rootPath, relativePath);
 87}
 88
 89function logInfo(message: string): void {
 490  process.stderr.write(`${message}\x1b[E\n`);
 91}
 92
 93function logError(error: Error): void {
 194  process.stderr.write(`\x1b[31m❌ Error: ${error.message}\x1b[0m\x1b[E\n`);
 195  if (error.stack) {
 196    process.stderr.write(`${error.stack}\x1b[E\n`);
 97  }
 98}
 99
 100function logTable(
 101  entries: ReadonlyArray<{ label: string; value: string }>,
 102): void {
 4103  const MAX_VALUE_WIDTH = 40;
 4104  const truncate = (s: string) =>
 26105    s.length > MAX_VALUE_WIDTH ? `…${s.slice(-(MAX_VALUE_WIDTH - 1))}` : s;
 106
 26107  const rows = entries.map(({ label, value }) => ({
 108    label,
 109    value: truncate(value),
 110  }));
 111
 26112  const maxLabel = Math.max(...rows.map((e) => e.label.length));
 26113  const maxValue = Math.max(...rows.map((e) => e.value.length));
 4114  const header = " 📁 Deployment Info ";
 4115  const innerWidth = maxLabel + maxValue + 4;
 4116  const padding = Math.max(0, innerWidth - header.length);
 117
 4118  const nl = "\x1b[E\n";
 119
 4120  process.stderr.write(nl);
 4121  process.stderr.write(`╭─${header}${"─".repeat(padding)}╮${nl}`);
 4122  for (const { label, value } of rows) {
 26123    process.stderr.write(
 124      `│ ${label.padEnd(maxLabel)} │ ${value.padEnd(maxValue)} │${nl}`,
 125    );
 126  }
 4127  process.stderr.write(
 128    `╰${"─".repeat(maxLabel + 2)}┴${"─".repeat(maxValue + 2)}╯${nl}`,
 129  );
 4130  process.stderr.write(nl);
 131}
 132
 133export function validateConfig(config: DeploymentConfig): void {
 7134  const errors: string[] = [];
 135
 7136  if (!config.repoName || config.repoName.trim() === "") {
 2137    errors.push("repoName is required and cannot be empty");
 138  }
 139
 7140  if (!config.branch || config.branch.trim() === "") {
 1141    errors.push("branch is required and cannot be empty");
 142  }
 143
 7144  if (config.environment === undefined || config.environment === null) {
 0145    errors.push("environment is required and cannot be empty");
 146  }
 147
 7148  if (!config.domain) {
 0149    errors.push("domain configuration is required");
 150  } else {
 7151    if (!config.domain.name || config.domain.name.trim() === "") {
 0152      errors.push("domain.name is required and cannot be empty");
 153    }
 7154    if (
 155      !config.domain.certificateId ||
 156      config.domain.certificateId.trim() === ""
 157    ) {
 0158      errors.push("domain.certificateId is required and cannot be empty");
 159    }
 7160    if (
 161      !config.domain.hostedZoneId ||
 162      config.domain.hostedZoneId.trim() === ""
 163    ) {
 0164      errors.push("domain.hostedZoneId is required and cannot be empty");
 165    }
 166  }
 167
 7168  if (!config.stacks) {
 0169    errors.push("stacks configuration is required");
 170  } else {
 7171    if (!config.stacks.frontend) {
 0172      errors.push("stacks.frontend is required");
 173    } else {
 7174      const { staticWebsites } = config.stacks.frontend;
 7175      if (staticWebsites) {
 7176        for (const [index, website] of staticWebsites.entries()) {
 2177          if (!website.name || website.name.trim() === "") {
 0178            errors.push(`frontend.staticWebsites[${index}].name is required`);
 179          }
 2180          if (!website.projectPath || website.projectPath.trim() === "") {
 0181            errors.push(
 182              `frontend.staticWebsites[${index}].projectPath is required`,
 183            );
 184          }
 185        }
 186      }
 187    }
 188  }
 189
 7190  if (errors.length > 0) {
 2191    throw new Error(
 192      `Configuration validation failed with ${errors.length} error(s):\n${errors.join("\n")}`,
 193    );
 194  }
 195}
 196
 197// ============================================================================
 198// Deployment
 199// ============================================================================
 200
 201export function deploy(configOverride?: DeploymentConfig): Stack[] {
 5202  const effectiveConfig = configOverride ?? config;
 5203  const rootPath = getRootPath(effectiveConfig.rootPath);
 204
 5205  try {
 5206    validateConfig(effectiveConfig);
 207
 208    // Log deployment info
 5209    const entries: Array<{ label: string; value: string }> = [
 210      { label: "Repository", value: effectiveConfig.repoName },
 211      { label: "Branch", value: effectiveConfig.branch },
 212      { label: "Environment", value: String(effectiveConfig.environment) },
 213    ];
 214
 5215    if (process.env.CDK_DEFAULT_REGION) {
 4216      entries.push({ label: "Region", value: process.env.CDK_DEFAULT_REGION });
 217    }
 4218    if (process.env.CDK_DEFAULT_ACCOUNT) {
 4219      entries.push({
 220        label: "Account",
 221        value: `***${process.env.CDK_DEFAULT_ACCOUNT.slice(-4)}`,
 222      });
 223    }
 224
 4225    entries.push({ label: "Root Path", value: rootPath });
 226
 4227    for (const ws of effectiveConfig.stacks.frontend.staticWebsites) {
 2228      entries.push({
 229        label: ws.name,
 230        value: resolveFullPath(rootPath, ws.projectPath),
 231      });
 232    }
 233
 4234    logTable(entries);
 235
 4236    logInfo("🎯 Requested stacks:");
 237
 4238    const app = new App();
 4239    const envFromCli: Environment = {
 240      account: process.env.CDK_DEFAULT_ACCOUNT,
 241      region: process.env.CDK_DEFAULT_REGION,
 242    };
 243
 4244    const stacks: Stack[] = [];
 245
 4246    for (const websiteConfig of effectiveConfig.stacks.frontend
 247      .staticWebsites) {
 2248      const distFolderPath = resolveFullPath(
 249        rootPath,
 250        websiteConfig.projectPath,
 251      );
 252
 2253      const stack = new StaticWebsiteStack(app, {
 254        env: envFromCli,
 255        name: websiteConfig.name,
 256        domains: [
 257          {
 258            subdomain: websiteConfig.subdomain,
 259            domainName: effectiveConfig.domain.name,
 260            certificateId: effectiveConfig.domain.certificateId,
 261            hostedZoneId: effectiveConfig.domain.hostedZoneId,
 262          },
 263        ],
 264        distFolderPath,
 265        envName: effectiveConfig.environment,
 266        githubRepo: effectiveConfig.repoName,
 267        stackName: `${effectiveConfig.repoName}-${websiteConfig.name}`,
 268      });
 269
 2270      stacks.push(stack);
 271    }
 272
 4273    const { publicFiles } = effectiveConfig.stacks;
 4274    if (publicFiles) {
 1275      stacks.push(
 276        new PublicFilesStack(app, {
 277          env: envFromCli,
 278          name: publicFiles.name,
 279          subdomain: publicFiles.subdomain,
 280          certificateId: publicFiles.certificateId,
 281          domainName: effectiveConfig.domain.name,
 282          hostedZoneId: effectiveConfig.domain.hostedZoneId,
 283          envName: effectiveConfig.environment,
 284          githubRepo: effectiveConfig.repoName,
 285          stackName: `${effectiveConfig.repoName}-${publicFiles.name}`,
 286        }),
 287      );
 288    }
 289
 4290    return stacks;
 291  } catch (error) {
 1292    if (error instanceof Error) {
 1293      logError(error);
 294    }
 1295    throw error;
 296  }
 297}
 298
 1299if (!process.env.VITEST) {
 0300  deploy();
 301}