| | | 1 | | #!/usr/bin/env node |
| | | 2 | | import path from "node:path"; |
| | | 3 | | import type { Environment, Stack } from "aws-cdk-lib"; |
| | | 4 | | /** |
| | | 5 | | * CDK Infrastructure Deployment Entry Point |
| | | 6 | | */ |
| | | 7 | | import { App } from "aws-cdk-lib"; |
| | | 8 | | import { AppEnvironment } from "../lib/core/types"; |
| | | 9 | | import { PublicFilesStack } from "../lib/stacks/publicFilesStack"; |
| | | 10 | | import { StaticWebsiteStack } from "../lib/stacks/staticWebsiteStack"; |
| | | 11 | | |
| | | 12 | | // ============================================================================ |
| | | 13 | | // Types |
| | | 14 | | // ============================================================================ |
| | | 15 | | |
| | | 16 | | interface StaticWebsiteConfig { |
| | | 17 | | name: string; |
| | | 18 | | projectPath: string; |
| | | 19 | | subdomain?: string; |
| | | 20 | | } |
| | | 21 | | |
| | | 22 | | interface PublicFilesConfig { |
| | | 23 | | name: string; |
| | | 24 | | subdomain: string; |
| | | 25 | | certificateId: string; |
| | | 26 | | } |
| | | 27 | | |
| | | 28 | | interface 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 | | |
| | 1 | 50 | | const 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 | | |
| | | 81 | | function getRootPath(rootPath?: string): string { |
| | 5 | 82 | | return rootPath ?? path.resolve(__dirname, "../../.."); |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | function resolveFullPath(rootPath: string, relativePath: string): string { |
| | 4 | 86 | | return path.join(rootPath, relativePath); |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | function logInfo(message: string): void { |
| | 4 | 90 | | process.stderr.write(`${message}\x1b[E\n`); |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | function logError(error: Error): void { |
| | 1 | 94 | | process.stderr.write(`\x1b[31m❌ Error: ${error.message}\x1b[0m\x1b[E\n`); |
| | 1 | 95 | | if (error.stack) { |
| | 1 | 96 | | process.stderr.write(`${error.stack}\x1b[E\n`); |
| | | 97 | | } |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | function logTable( |
| | | 101 | | entries: ReadonlyArray<{ label: string; value: string }>, |
| | | 102 | | ): void { |
| | 4 | 103 | | const MAX_VALUE_WIDTH = 40; |
| | 4 | 104 | | const truncate = (s: string) => |
| | 26 | 105 | | s.length > MAX_VALUE_WIDTH ? `…${s.slice(-(MAX_VALUE_WIDTH - 1))}` : s; |
| | | 106 | | |
| | 26 | 107 | | const rows = entries.map(({ label, value }) => ({ |
| | | 108 | | label, |
| | | 109 | | value: truncate(value), |
| | | 110 | | })); |
| | | 111 | | |
| | 26 | 112 | | const maxLabel = Math.max(...rows.map((e) => e.label.length)); |
| | 26 | 113 | | const maxValue = Math.max(...rows.map((e) => e.value.length)); |
| | 4 | 114 | | const header = " 📁 Deployment Info "; |
| | 4 | 115 | | const innerWidth = maxLabel + maxValue + 4; |
| | 4 | 116 | | const padding = Math.max(0, innerWidth - header.length); |
| | | 117 | | |
| | 4 | 118 | | const nl = "\x1b[E\n"; |
| | | 119 | | |
| | 4 | 120 | | process.stderr.write(nl); |
| | 4 | 121 | | process.stderr.write(`╭─${header}${"─".repeat(padding)}╮${nl}`); |
| | 4 | 122 | | for (const { label, value } of rows) { |
| | 26 | 123 | | process.stderr.write( |
| | | 124 | | `│ ${label.padEnd(maxLabel)} │ ${value.padEnd(maxValue)} │${nl}`, |
| | | 125 | | ); |
| | | 126 | | } |
| | 4 | 127 | | process.stderr.write( |
| | | 128 | | `╰${"─".repeat(maxLabel + 2)}┴${"─".repeat(maxValue + 2)}╯${nl}`, |
| | | 129 | | ); |
| | 4 | 130 | | process.stderr.write(nl); |
| | | 131 | | } |
| | | 132 | | |
| | | 133 | | export function validateConfig(config: DeploymentConfig): void { |
| | 7 | 134 | | const errors: string[] = []; |
| | | 135 | | |
| | 7 | 136 | | if (!config.repoName || config.repoName.trim() === "") { |
| | 2 | 137 | | errors.push("repoName is required and cannot be empty"); |
| | | 138 | | } |
| | | 139 | | |
| | 7 | 140 | | if (!config.branch || config.branch.trim() === "") { |
| | 1 | 141 | | errors.push("branch is required and cannot be empty"); |
| | | 142 | | } |
| | | 143 | | |
| | 7 | 144 | | if (config.environment === undefined || config.environment === null) { |
| | 0 | 145 | | errors.push("environment is required and cannot be empty"); |
| | | 146 | | } |
| | | 147 | | |
| | 7 | 148 | | if (!config.domain) { |
| | 0 | 149 | | errors.push("domain configuration is required"); |
| | | 150 | | } else { |
| | 7 | 151 | | if (!config.domain.name || config.domain.name.trim() === "") { |
| | 0 | 152 | | errors.push("domain.name is required and cannot be empty"); |
| | | 153 | | } |
| | 7 | 154 | | if ( |
| | | 155 | | !config.domain.certificateId || |
| | | 156 | | config.domain.certificateId.trim() === "" |
| | | 157 | | ) { |
| | 0 | 158 | | errors.push("domain.certificateId is required and cannot be empty"); |
| | | 159 | | } |
| | 7 | 160 | | if ( |
| | | 161 | | !config.domain.hostedZoneId || |
| | | 162 | | config.domain.hostedZoneId.trim() === "" |
| | | 163 | | ) { |
| | 0 | 164 | | errors.push("domain.hostedZoneId is required and cannot be empty"); |
| | | 165 | | } |
| | | 166 | | } |
| | | 167 | | |
| | 7 | 168 | | if (!config.stacks) { |
| | 0 | 169 | | errors.push("stacks configuration is required"); |
| | | 170 | | } else { |
| | 7 | 171 | | if (!config.stacks.frontend) { |
| | 0 | 172 | | errors.push("stacks.frontend is required"); |
| | | 173 | | } else { |
| | 7 | 174 | | const { staticWebsites } = config.stacks.frontend; |
| | 7 | 175 | | if (staticWebsites) { |
| | 7 | 176 | | for (const [index, website] of staticWebsites.entries()) { |
| | 2 | 177 | | if (!website.name || website.name.trim() === "") { |
| | 0 | 178 | | errors.push(`frontend.staticWebsites[${index}].name is required`); |
| | | 179 | | } |
| | 2 | 180 | | if (!website.projectPath || website.projectPath.trim() === "") { |
| | 0 | 181 | | errors.push( |
| | | 182 | | `frontend.staticWebsites[${index}].projectPath is required`, |
| | | 183 | | ); |
| | | 184 | | } |
| | | 185 | | } |
| | | 186 | | } |
| | | 187 | | } |
| | | 188 | | } |
| | | 189 | | |
| | 7 | 190 | | if (errors.length > 0) { |
| | 2 | 191 | | 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 | | |
| | | 201 | | export function deploy(configOverride?: DeploymentConfig): Stack[] { |
| | 5 | 202 | | const effectiveConfig = configOverride ?? config; |
| | 5 | 203 | | const rootPath = getRootPath(effectiveConfig.rootPath); |
| | | 204 | | |
| | 5 | 205 | | try { |
| | 5 | 206 | | validateConfig(effectiveConfig); |
| | | 207 | | |
| | | 208 | | // Log deployment info |
| | 5 | 209 | | 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 | | |
| | 5 | 215 | | if (process.env.CDK_DEFAULT_REGION) { |
| | 4 | 216 | | entries.push({ label: "Region", value: process.env.CDK_DEFAULT_REGION }); |
| | | 217 | | } |
| | 4 | 218 | | if (process.env.CDK_DEFAULT_ACCOUNT) { |
| | 4 | 219 | | entries.push({ |
| | | 220 | | label: "Account", |
| | | 221 | | value: `***${process.env.CDK_DEFAULT_ACCOUNT.slice(-4)}`, |
| | | 222 | | }); |
| | | 223 | | } |
| | | 224 | | |
| | 4 | 225 | | entries.push({ label: "Root Path", value: rootPath }); |
| | | 226 | | |
| | 4 | 227 | | for (const ws of effectiveConfig.stacks.frontend.staticWebsites) { |
| | 2 | 228 | | entries.push({ |
| | | 229 | | label: ws.name, |
| | | 230 | | value: resolveFullPath(rootPath, ws.projectPath), |
| | | 231 | | }); |
| | | 232 | | } |
| | | 233 | | |
| | 4 | 234 | | logTable(entries); |
| | | 235 | | |
| | 4 | 236 | | logInfo("🎯 Requested stacks:"); |
| | | 237 | | |
| | 4 | 238 | | const app = new App(); |
| | 4 | 239 | | const envFromCli: Environment = { |
| | | 240 | | account: process.env.CDK_DEFAULT_ACCOUNT, |
| | | 241 | | region: process.env.CDK_DEFAULT_REGION, |
| | | 242 | | }; |
| | | 243 | | |
| | 4 | 244 | | const stacks: Stack[] = []; |
| | | 245 | | |
| | 4 | 246 | | for (const websiteConfig of effectiveConfig.stacks.frontend |
| | | 247 | | .staticWebsites) { |
| | 2 | 248 | | const distFolderPath = resolveFullPath( |
| | | 249 | | rootPath, |
| | | 250 | | websiteConfig.projectPath, |
| | | 251 | | ); |
| | | 252 | | |
| | 2 | 253 | | 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 | | |
| | 2 | 270 | | stacks.push(stack); |
| | | 271 | | } |
| | | 272 | | |
| | 4 | 273 | | const { publicFiles } = effectiveConfig.stacks; |
| | 4 | 274 | | if (publicFiles) { |
| | 1 | 275 | | 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 | | |
| | 4 | 290 | | return stacks; |
| | | 291 | | } catch (error) { |
| | 1 | 292 | | if (error instanceof Error) { |
| | 1 | 293 | | logError(error); |
| | | 294 | | } |
| | 1 | 295 | | throw error; |
| | | 296 | | } |
| | | 297 | | } |
| | | 298 | | |
| | 1 | 299 | | if (!process.env.VITEST) { |
| | 0 | 300 | | deploy(); |
| | | 301 | | } |