@@ -8,23 +8,37 @@ import { CloudFrontClient } from '../../src/aws/cloudfront'
88import { ECRClient } from '../../src/aws/ecr'
99import { ECSClient } from '../../src/aws/ecs'
1010import { validateTemplate , validateTemplateSize , validateResourceLimits } from '../../src/validation/template'
11- import { loadValidatedConfig } from './shared'
11+ import { loadValidatedConfig , resolveDnsProviderConfig , getDnsProvider } from './shared'
12+ import { deployStaticSiteWithExternalDnsFull } from '../../src/deploy/static-site-external-dns'
13+ import type { DnsProviderConfig } from '../../src/dns/types'
1214
1315export function registerDeployCommands ( app : CLI ) : void {
1416 app
1517 . command ( 'deploy' , 'Deploy infrastructure' )
1618 . option ( '--stack <name>' , 'Stack name' )
1719 . option ( '--env <environment>' , 'Environment to deploy to' )
18- . action ( async ( options ?: { stack ?: string , env ?: string } ) => {
20+ . option ( '--site <name>' , 'Deploy specific site only' )
21+ . action ( async ( options ?: { stack ?: string , env ?: string , site ?: string } ) => {
1922 cli . header ( 'Deploying Infrastructure' )
2023
2124 try {
2225 // Load configuration
2326 const config = await loadValidatedConfig ( )
24- const environment = ( options ?. env || 'production ' ) as 'production' | 'staging' | 'development'
27+ const environment = ( options ?. env || 'staging ' ) as 'production' | 'staging' | 'development'
2528 const stackName = options ?. stack || `${ config . project . slug } -${ environment } `
2629 const region = config . project . region || 'us-east-1'
2730
31+ // Check if this is a static site deployment
32+ if ( config . sites && Object . keys ( config . sites ) . length > 0 ) {
33+ const dnsProvider = config . infrastructure ?. dns ?. provider
34+
35+ if ( dnsProvider && dnsProvider !== 'route53' ) {
36+ // Deploy static sites with external DNS
37+ await deployStaticSitesWithExternalDns ( config , options ?. site , dnsProvider , region )
38+ return
39+ }
40+ }
41+
2842 cli . info ( `Stack: ${ stackName } ` )
2943 cli . info ( `Region: ${ region } ` )
3044 cli . info ( `Environment: ${ environment } ` )
@@ -672,3 +686,165 @@ https://${bucket}.s3.${region}.amazonaws.com${prefix ? `/${prefix}` : ''}/index.
672686 }
673687 } )
674688}
689+
690+ /**
691+ * Deploy static sites with external DNS provider (Cloudflare, Porkbun, GoDaddy)
692+ * Handles detection of existing DNS records (like Netlify) and prompts for migration
693+ */
694+ async function deployStaticSitesWithExternalDns (
695+ config : any ,
696+ specificSite : string | undefined ,
697+ dnsProviderName : string ,
698+ region : string ,
699+ ) : Promise < void > {
700+ const sites = config . sites || { }
701+ const siteNames = specificSite ? [ specificSite ] : Object . keys ( sites )
702+
703+ if ( siteNames . length === 0 ) {
704+ cli . warn ( 'No sites configured in cloud.config.ts' )
705+ return
706+ }
707+
708+ // Get DNS provider config from environment
709+ const dnsConfig = resolveDnsProviderConfig ( dnsProviderName )
710+ if ( ! dnsConfig ) {
711+ cli . error ( `DNS provider '${ dnsProviderName } ' is not configured. Please set the required environment variables.` )
712+ cli . info ( '\nFor Cloudflare: CLOUDFLARE_API_TOKEN' )
713+ cli . info ( 'For Porkbun: PORKBUN_API_KEY, PORKBUN_SECRET_KEY' )
714+ cli . info ( 'For GoDaddy: GODADDY_API_KEY, GODADDY_API_SECRET' )
715+ return
716+ }
717+
718+ const dnsProvider = getDnsProvider ( dnsProviderName )
719+
720+ for ( const siteName of siteNames ) {
721+ const siteConfig = sites [ siteName ]
722+ if ( ! siteConfig ) {
723+ cli . error ( `Site '${ siteName } ' not found in configuration` )
724+ continue
725+ }
726+
727+ const domain = siteConfig . domain
728+ if ( ! domain ) {
729+ cli . error ( `Site '${ siteName } ' has no domain configured` )
730+ continue
731+ }
732+
733+ cli . header ( `Deploying Site: ${ siteName } ` )
734+ cli . info ( `Domain: ${ domain } ` )
735+ cli . info ( `Source: ${ siteConfig . root } ` )
736+ cli . info ( `DNS Provider: ${ dnsProviderName } ` )
737+
738+ // Check if source directory exists
739+ if ( ! existsSync ( siteConfig . root ) ) {
740+ cli . error ( `Source directory not found: ${ siteConfig . root } ` )
741+ cli . info ( 'Run your build command first (e.g., bun run generate)' )
742+ continue
743+ }
744+
745+ // Check for existing DNS records
746+ cli . step ( 'Checking existing DNS records...' )
747+ const existingRecords = await dnsProvider . listRecords ( domain )
748+
749+ if ( existingRecords . success && existingRecords . records . length > 0 ) {
750+ // Look for existing CNAME records that might be pointing to Netlify or other providers
751+ const domainParts = domain . split ( '.' )
752+ const subdomain = domainParts . length > 2 ? domainParts [ 0 ] : '@'
753+ const rootDomain = domainParts . slice ( - 2 ) . join ( '.' )
754+
755+ const existingCname = existingRecords . records . find ( r =>
756+ r . type === 'CNAME' &&
757+ ( r . name === domain || r . name === subdomain || r . name === `${ subdomain } .${ rootDomain } ` )
758+ )
759+
760+ if ( existingCname ) {
761+ const isNetlify = existingCname . content . includes ( 'netlify' )
762+ const isVercel = existingCname . content . includes ( 'vercel' )
763+ const isCloudFront = existingCname . content . includes ( 'cloudfront.net' )
764+
765+ // Skip if already pointing to CloudFront (our infrastructure)
766+ if ( isCloudFront ) {
767+ cli . info ( `Domain already points to CloudFront: ${ existingCname . content } ` )
768+ cli . info ( 'Proceeding with file upload...' )
769+ } else {
770+ const providerName = isNetlify ? 'Netlify' : isVercel ? 'Vercel' : 'another provider'
771+
772+ cli . warn ( `\nExisting CNAME record detected:` )
773+ cli . info ( ` ${ existingCname . name } -> ${ existingCname . content } ` )
774+
775+ if ( isNetlify || isVercel ) {
776+ cli . info ( `\nThis domain is currently pointing to ${ providerName } .` )
777+ cli . info ( 'Deploying will update this record to point to AWS CloudFront.' )
778+ }
779+
780+ const proceed = await cli . confirm ( `\nUpdate DNS record to point to AWS CloudFront?` , true )
781+ if ( ! proceed ) {
782+ cli . info ( 'Deployment cancelled' )
783+ continue
784+ }
785+
786+ // Delete the old CNAME record before deploying
787+ cli . step ( `Removing old ${ providerName } CNAME record...` )
788+ const deleteResult = await dnsProvider . deleteRecord ( domain , {
789+ name : existingCname . name ,
790+ type : 'CNAME' ,
791+ content : existingCname . content ,
792+ } )
793+
794+ if ( deleteResult . success ) {
795+ cli . success ( `Removed CNAME: ${ existingCname . name } -> ${ existingCname . content } ` )
796+ } else {
797+ cli . warn ( `Could not remove old record: ${ deleteResult . message } ` )
798+ cli . info ( 'The deployment will attempt to update it instead.' )
799+ }
800+ }
801+ }
802+ }
803+
804+ // Deploy the static site
805+ cli . step ( 'Deploying to AWS (S3 + CloudFront)...' )
806+
807+ const result = await deployStaticSiteWithExternalDnsFull ( {
808+ siteName : `${ config . project . slug } -${ siteName } ` ,
809+ domain,
810+ region,
811+ sourceDir : siteConfig . root ,
812+ certificateArn : siteConfig . certificateArn ,
813+ dnsProvider : dnsConfig ,
814+ onProgress : ( stage , detail ) => {
815+ if ( stage === 'infrastructure' ) {
816+ cli . step ( detail || 'Setting up infrastructure...' )
817+ } else if ( stage === 'upload' ) {
818+ // Show upload progress without spamming
819+ if ( detail ?. includes ( '/' ) && ! detail . includes ( '1/' ) ) {
820+ const match = detail . match ( / ( \d + ) \/ ( \d + ) / )
821+ if ( match ) {
822+ const [ , current , total ] = match
823+ if ( Number ( current ) % 10 === 0 || current === total ) {
824+ cli . info ( ` Uploaded ${ current } /${ total } files` )
825+ }
826+ }
827+ }
828+ } else if ( stage === 'invalidate' ) {
829+ cli . step ( 'Invalidating CDN cache...' )
830+ } else if ( stage === 'complete' ) {
831+ // Handled below
832+ }
833+ } ,
834+ } )
835+
836+ if ( result . success ) {
837+ cli . success ( '\nDeployment successful!' )
838+ cli . box ( `Site Deployed!
839+
840+ Domain: https://${ result . domain }
841+ CloudFront: ${ result . distributionDomain }
842+ Bucket: ${ result . bucket }
843+ Files: ${ result . filesUploaded }
844+
845+ Your site is now live at https://${ result . domain } ` , 'green' )
846+ } else {
847+ cli . error ( `\nDeployment failed: ${ result . message } ` )
848+ }
849+ }
850+ }
0 commit comments