@stacksjs/ts-cloud-core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodeDeployApplication,
|
|
3
|
+
CodeDeployDeploymentGroup,
|
|
4
|
+
CodeDeployDeploymentConfig,
|
|
5
|
+
} from '@stacksjs/ts-cloud-aws-types'
|
|
6
|
+
import type { EnvironmentType } from '@stacksjs/ts-cloud-types'
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, renameSync, copyFileSync } from 'node:fs'
|
|
9
|
+
import { join, basename, dirname, extname } from 'node:path'
|
|
10
|
+
import { Fn } from '../intrinsic-functions'
|
|
11
|
+
import { generateLogicalId, generateResourceName } from '../resource-naming'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Asset file with hash information
|
|
15
|
+
*/
|
|
16
|
+
export interface HashedAsset {
|
|
17
|
+
originalPath: string
|
|
18
|
+
hashedPath: string
|
|
19
|
+
hash: string
|
|
20
|
+
size: number
|
|
21
|
+
contentType: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Asset manifest for deployment
|
|
26
|
+
*/
|
|
27
|
+
export interface AssetManifest {
|
|
28
|
+
version: string
|
|
29
|
+
timestamp: string
|
|
30
|
+
assets: HashedAsset[]
|
|
31
|
+
hashMap: Record<string, string> // original -> hashed path mapping
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CodeDeployApplicationOptions {
|
|
35
|
+
slug: string
|
|
36
|
+
environment: EnvironmentType
|
|
37
|
+
applicationName?: string
|
|
38
|
+
computePlatform: 'Server' | 'Lambda' | 'ECS'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CodeDeployDeploymentGroupOptions {
|
|
42
|
+
slug: string
|
|
43
|
+
environment: EnvironmentType
|
|
44
|
+
deploymentGroupName?: string
|
|
45
|
+
serviceRoleArn: string
|
|
46
|
+
autoScalingGroups?: string[]
|
|
47
|
+
ec2TagFilters?: Array<{
|
|
48
|
+
key?: string
|
|
49
|
+
value?: string
|
|
50
|
+
type?: 'KEY_ONLY' | 'VALUE_ONLY' | 'KEY_AND_VALUE'
|
|
51
|
+
}>
|
|
52
|
+
deploymentConfigName?: string
|
|
53
|
+
autoRollbackConfiguration?: {
|
|
54
|
+
enabled: boolean
|
|
55
|
+
events?: ('DEPLOYMENT_FAILURE' | 'DEPLOYMENT_STOP_ON_ALARM' | 'DEPLOYMENT_STOP_ON_REQUEST')[]
|
|
56
|
+
}
|
|
57
|
+
alarmConfiguration?: {
|
|
58
|
+
enabled: boolean
|
|
59
|
+
alarms?: Array<{
|
|
60
|
+
name: string
|
|
61
|
+
}>
|
|
62
|
+
ignorePollAlarmFailure?: boolean
|
|
63
|
+
}
|
|
64
|
+
loadBalancerInfo?: {
|
|
65
|
+
targetGroupInfoList?: Array<{
|
|
66
|
+
name: string
|
|
67
|
+
}>
|
|
68
|
+
elbInfoList?: Array<{
|
|
69
|
+
name: string
|
|
70
|
+
}>
|
|
71
|
+
}
|
|
72
|
+
blueGreenDeploymentConfiguration?: {
|
|
73
|
+
terminateBlueInstancesOnDeploymentSuccess?: {
|
|
74
|
+
action?: 'TERMINATE' | 'KEEP_ALIVE'
|
|
75
|
+
terminationWaitTimeInMinutes?: number
|
|
76
|
+
}
|
|
77
|
+
deploymentReadyOption?: {
|
|
78
|
+
actionOnTimeout?: 'CONTINUE_DEPLOYMENT' | 'STOP_DEPLOYMENT'
|
|
79
|
+
waitTimeInMinutes?: number
|
|
80
|
+
}
|
|
81
|
+
greenFleetProvisioningOption?: {
|
|
82
|
+
action?: 'DISCOVER_EXISTING' | 'COPY_AUTO_SCALING_GROUP'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CodeDeployDeploymentConfigOptions {
|
|
88
|
+
slug: string
|
|
89
|
+
environment: EnvironmentType
|
|
90
|
+
deploymentConfigName?: string
|
|
91
|
+
minimumHealthyHosts?: {
|
|
92
|
+
type: 'HOST_COUNT' | 'FLEET_PERCENT'
|
|
93
|
+
value: number
|
|
94
|
+
}
|
|
95
|
+
trafficRoutingConfig?: {
|
|
96
|
+
type: 'TimeBasedCanary' | 'TimeBasedLinear' | 'AllAtOnce'
|
|
97
|
+
timeBasedCanary?: {
|
|
98
|
+
canaryPercentage: number
|
|
99
|
+
canaryInterval: number
|
|
100
|
+
}
|
|
101
|
+
timeBasedLinear?: {
|
|
102
|
+
linearPercentage: number
|
|
103
|
+
linearInterval: number
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DeploymentStrategyOptions {
|
|
109
|
+
type: 'rolling' | 'blue-green' | 'canary' | 'all-at-once'
|
|
110
|
+
batchSize?: number
|
|
111
|
+
batchPercentage?: number
|
|
112
|
+
canaryPercentage?: number
|
|
113
|
+
canaryInterval?: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Deployment Module - CodeDeploy and Deployment Utilities
|
|
118
|
+
* Provides clean API for deployment infrastructure and strategies
|
|
119
|
+
*/
|
|
120
|
+
export class Deployment {
|
|
121
|
+
/**
|
|
122
|
+
* Create a CodeDeploy Application
|
|
123
|
+
*/
|
|
124
|
+
static createApplication(options: CodeDeployApplicationOptions): {
|
|
125
|
+
application: CodeDeployApplication
|
|
126
|
+
logicalId: string
|
|
127
|
+
} {
|
|
128
|
+
const {
|
|
129
|
+
slug,
|
|
130
|
+
environment,
|
|
131
|
+
applicationName,
|
|
132
|
+
computePlatform,
|
|
133
|
+
} = options
|
|
134
|
+
|
|
135
|
+
const resourceName = applicationName || generateResourceName({
|
|
136
|
+
slug,
|
|
137
|
+
environment,
|
|
138
|
+
resourceType: 'deploy-app',
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const logicalId = generateLogicalId(resourceName)
|
|
142
|
+
|
|
143
|
+
const application: CodeDeployApplication = {
|
|
144
|
+
Type: 'AWS::CodeDeploy::Application',
|
|
145
|
+
Properties: {
|
|
146
|
+
ApplicationName: resourceName,
|
|
147
|
+
ComputePlatform: computePlatform,
|
|
148
|
+
Tags: [
|
|
149
|
+
{ Key: 'Name', Value: resourceName },
|
|
150
|
+
{ Key: 'Environment', Value: environment },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { application, logicalId }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a CodeDeploy Deployment Group
|
|
160
|
+
*/
|
|
161
|
+
static createDeploymentGroup(
|
|
162
|
+
applicationLogicalId: string,
|
|
163
|
+
options: CodeDeployDeploymentGroupOptions,
|
|
164
|
+
): {
|
|
165
|
+
deploymentGroup: CodeDeployDeploymentGroup
|
|
166
|
+
logicalId: string
|
|
167
|
+
} {
|
|
168
|
+
const {
|
|
169
|
+
slug,
|
|
170
|
+
environment,
|
|
171
|
+
deploymentGroupName,
|
|
172
|
+
serviceRoleArn,
|
|
173
|
+
autoScalingGroups,
|
|
174
|
+
ec2TagFilters,
|
|
175
|
+
deploymentConfigName,
|
|
176
|
+
autoRollbackConfiguration,
|
|
177
|
+
alarmConfiguration,
|
|
178
|
+
loadBalancerInfo,
|
|
179
|
+
blueGreenDeploymentConfiguration,
|
|
180
|
+
} = options
|
|
181
|
+
|
|
182
|
+
const resourceName = deploymentGroupName || generateResourceName({
|
|
183
|
+
slug,
|
|
184
|
+
environment,
|
|
185
|
+
resourceType: 'deploy-group',
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const logicalId = generateLogicalId(resourceName)
|
|
189
|
+
|
|
190
|
+
const deploymentGroup: CodeDeployDeploymentGroup = {
|
|
191
|
+
Type: 'AWS::CodeDeploy::DeploymentGroup',
|
|
192
|
+
Properties: {
|
|
193
|
+
ApplicationName: Fn.Ref(applicationLogicalId) as unknown as string,
|
|
194
|
+
DeploymentGroupName: resourceName,
|
|
195
|
+
ServiceRoleArn: serviceRoleArn,
|
|
196
|
+
AutoScalingGroups: autoScalingGroups,
|
|
197
|
+
Ec2TagFilters: ec2TagFilters?.map(f => ({
|
|
198
|
+
Key: f.key,
|
|
199
|
+
Value: f.value,
|
|
200
|
+
Type: f.type,
|
|
201
|
+
})),
|
|
202
|
+
DeploymentConfigName: deploymentConfigName,
|
|
203
|
+
AutoRollbackConfiguration: autoRollbackConfiguration ? {
|
|
204
|
+
Enabled: autoRollbackConfiguration.enabled,
|
|
205
|
+
Events: autoRollbackConfiguration.events,
|
|
206
|
+
} : undefined,
|
|
207
|
+
AlarmConfiguration: alarmConfiguration ? {
|
|
208
|
+
Enabled: alarmConfiguration.enabled,
|
|
209
|
+
Alarms: alarmConfiguration.alarms?.map(a => ({ Name: a.name })),
|
|
210
|
+
IgnorePollAlarmFailure: alarmConfiguration.ignorePollAlarmFailure,
|
|
211
|
+
} : undefined,
|
|
212
|
+
LoadBalancerInfo: loadBalancerInfo ? {
|
|
213
|
+
TargetGroupInfoList: loadBalancerInfo.targetGroupInfoList?.map(t => ({ Name: t.name })),
|
|
214
|
+
ElbInfoList: loadBalancerInfo.elbInfoList?.map(e => ({ Name: e.name })),
|
|
215
|
+
} : undefined,
|
|
216
|
+
BlueGreenDeploymentConfiguration: blueGreenDeploymentConfiguration ? {
|
|
217
|
+
TerminateBlueInstancesOnDeploymentSuccess: blueGreenDeploymentConfiguration.terminateBlueInstancesOnDeploymentSuccess ? {
|
|
218
|
+
Action: blueGreenDeploymentConfiguration.terminateBlueInstancesOnDeploymentSuccess.action,
|
|
219
|
+
TerminationWaitTimeInMinutes: blueGreenDeploymentConfiguration.terminateBlueInstancesOnDeploymentSuccess.terminationWaitTimeInMinutes,
|
|
220
|
+
} : undefined,
|
|
221
|
+
DeploymentReadyOption: blueGreenDeploymentConfiguration.deploymentReadyOption ? {
|
|
222
|
+
ActionOnTimeout: blueGreenDeploymentConfiguration.deploymentReadyOption.actionOnTimeout,
|
|
223
|
+
WaitTimeInMinutes: blueGreenDeploymentConfiguration.deploymentReadyOption.waitTimeInMinutes,
|
|
224
|
+
} : undefined,
|
|
225
|
+
GreenFleetProvisioningOption: blueGreenDeploymentConfiguration.greenFleetProvisioningOption ? {
|
|
226
|
+
Action: blueGreenDeploymentConfiguration.greenFleetProvisioningOption.action,
|
|
227
|
+
} : undefined,
|
|
228
|
+
} : undefined,
|
|
229
|
+
Tags: [
|
|
230
|
+
{ Key: 'Name', Value: resourceName },
|
|
231
|
+
{ Key: 'Environment', Value: environment },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { deploymentGroup, logicalId }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a CodeDeploy Deployment Configuration
|
|
241
|
+
*/
|
|
242
|
+
static createDeploymentConfig(options: CodeDeployDeploymentConfigOptions): {
|
|
243
|
+
deploymentConfig: CodeDeployDeploymentConfig
|
|
244
|
+
logicalId: string
|
|
245
|
+
} {
|
|
246
|
+
const {
|
|
247
|
+
slug,
|
|
248
|
+
environment,
|
|
249
|
+
deploymentConfigName,
|
|
250
|
+
minimumHealthyHosts,
|
|
251
|
+
trafficRoutingConfig,
|
|
252
|
+
} = options
|
|
253
|
+
|
|
254
|
+
const resourceName = deploymentConfigName || generateResourceName({
|
|
255
|
+
slug,
|
|
256
|
+
environment,
|
|
257
|
+
resourceType: 'deploy-config',
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const logicalId = generateLogicalId(resourceName)
|
|
261
|
+
|
|
262
|
+
const deploymentConfig: CodeDeployDeploymentConfig = {
|
|
263
|
+
Type: 'AWS::CodeDeploy::DeploymentConfig',
|
|
264
|
+
Properties: {
|
|
265
|
+
DeploymentConfigName: resourceName,
|
|
266
|
+
MinimumHealthyHosts: minimumHealthyHosts ? {
|
|
267
|
+
Type: minimumHealthyHosts.type,
|
|
268
|
+
Value: minimumHealthyHosts.value,
|
|
269
|
+
} : undefined,
|
|
270
|
+
TrafficRoutingConfig: trafficRoutingConfig ? {
|
|
271
|
+
Type: trafficRoutingConfig.type,
|
|
272
|
+
TimeBasedCanary: trafficRoutingConfig.timeBasedCanary ? {
|
|
273
|
+
CanaryPercentage: trafficRoutingConfig.timeBasedCanary.canaryPercentage,
|
|
274
|
+
CanaryInterval: trafficRoutingConfig.timeBasedCanary.canaryInterval,
|
|
275
|
+
} : undefined,
|
|
276
|
+
TimeBasedLinear: trafficRoutingConfig.timeBasedLinear ? {
|
|
277
|
+
LinearPercentage: trafficRoutingConfig.timeBasedLinear.linearPercentage,
|
|
278
|
+
LinearInterval: trafficRoutingConfig.timeBasedLinear.linearInterval,
|
|
279
|
+
} : undefined,
|
|
280
|
+
} : undefined,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { deploymentConfig, logicalId }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Common deployment configurations
|
|
289
|
+
*/
|
|
290
|
+
static readonly DeploymentConfigs = {
|
|
291
|
+
/**
|
|
292
|
+
* All at once deployment (fastest, but downtime)
|
|
293
|
+
*/
|
|
294
|
+
allAtOnce: (): CodeDeployDeploymentConfigOptions['minimumHealthyHosts'] => ({
|
|
295
|
+
type: 'FLEET_PERCENT',
|
|
296
|
+
value: 0,
|
|
297
|
+
}),
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Half at a time deployment
|
|
301
|
+
*/
|
|
302
|
+
halfAtATime: (): CodeDeployDeploymentConfigOptions['minimumHealthyHosts'] => ({
|
|
303
|
+
type: 'FLEET_PERCENT',
|
|
304
|
+
value: 50,
|
|
305
|
+
}),
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* One at a time deployment (slowest, but safest)
|
|
309
|
+
*/
|
|
310
|
+
oneAtATime: (): CodeDeployDeploymentConfigOptions['minimumHealthyHosts'] => ({
|
|
311
|
+
type: 'HOST_COUNT',
|
|
312
|
+
value: 1,
|
|
313
|
+
}),
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Custom deployment configuration
|
|
317
|
+
*/
|
|
318
|
+
custom: (
|
|
319
|
+
type: 'HOST_COUNT' | 'FLEET_PERCENT',
|
|
320
|
+
value: number,
|
|
321
|
+
): CodeDeployDeploymentConfigOptions['minimumHealthyHosts'] => ({
|
|
322
|
+
type,
|
|
323
|
+
value,
|
|
324
|
+
}),
|
|
325
|
+
} as const
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Traffic routing configurations
|
|
329
|
+
*/
|
|
330
|
+
static readonly TrafficRouting = {
|
|
331
|
+
/**
|
|
332
|
+
* All traffic at once
|
|
333
|
+
*/
|
|
334
|
+
allAtOnce: (): CodeDeployDeploymentConfigOptions['trafficRoutingConfig'] => ({
|
|
335
|
+
type: 'AllAtOnce',
|
|
336
|
+
}),
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Canary deployment (shift traffic in two steps)
|
|
340
|
+
*/
|
|
341
|
+
canary: (
|
|
342
|
+
canaryPercentage: number,
|
|
343
|
+
canaryInterval: number,
|
|
344
|
+
): CodeDeployDeploymentConfigOptions['trafficRoutingConfig'] => ({
|
|
345
|
+
type: 'TimeBasedCanary',
|
|
346
|
+
timeBasedCanary: {
|
|
347
|
+
canaryPercentage,
|
|
348
|
+
canaryInterval,
|
|
349
|
+
},
|
|
350
|
+
}),
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Linear deployment (shift traffic gradually)
|
|
354
|
+
*/
|
|
355
|
+
linear: (
|
|
356
|
+
linearPercentage: number,
|
|
357
|
+
linearInterval: number,
|
|
358
|
+
): CodeDeployDeploymentConfigOptions['trafficRoutingConfig'] => ({
|
|
359
|
+
type: 'TimeBasedLinear',
|
|
360
|
+
timeBasedLinear: {
|
|
361
|
+
linearPercentage,
|
|
362
|
+
linearInterval,
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
} as const
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Rollback configurations
|
|
369
|
+
*/
|
|
370
|
+
static readonly RollbackConfigs = {
|
|
371
|
+
/**
|
|
372
|
+
* Auto rollback on deployment failure
|
|
373
|
+
*/
|
|
374
|
+
onFailure: (): CodeDeployDeploymentGroupOptions['autoRollbackConfiguration'] => ({
|
|
375
|
+
enabled: true,
|
|
376
|
+
events: ['DEPLOYMENT_FAILURE'],
|
|
377
|
+
}),
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Auto rollback on alarm or failure
|
|
381
|
+
*/
|
|
382
|
+
onAlarmOrFailure: (): CodeDeployDeploymentGroupOptions['autoRollbackConfiguration'] => ({
|
|
383
|
+
enabled: true,
|
|
384
|
+
events: ['DEPLOYMENT_FAILURE', 'DEPLOYMENT_STOP_ON_ALARM'],
|
|
385
|
+
}),
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Auto rollback on all events
|
|
389
|
+
*/
|
|
390
|
+
onAllEvents: (): CodeDeployDeploymentGroupOptions['autoRollbackConfiguration'] => ({
|
|
391
|
+
enabled: true,
|
|
392
|
+
events: ['DEPLOYMENT_FAILURE', 'DEPLOYMENT_STOP_ON_ALARM', 'DEPLOYMENT_STOP_ON_REQUEST'],
|
|
393
|
+
}),
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* No auto rollback
|
|
397
|
+
*/
|
|
398
|
+
disabled: (): CodeDeployDeploymentGroupOptions['autoRollbackConfiguration'] => ({
|
|
399
|
+
enabled: false,
|
|
400
|
+
}),
|
|
401
|
+
} as const
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Blue/Green deployment configurations
|
|
405
|
+
*/
|
|
406
|
+
static readonly BlueGreenConfigs = {
|
|
407
|
+
/**
|
|
408
|
+
* Standard blue/green with immediate termination
|
|
409
|
+
*/
|
|
410
|
+
standard: (): CodeDeployDeploymentGroupOptions['blueGreenDeploymentConfiguration'] => ({
|
|
411
|
+
terminateBlueInstancesOnDeploymentSuccess: {
|
|
412
|
+
action: 'TERMINATE',
|
|
413
|
+
terminationWaitTimeInMinutes: 5,
|
|
414
|
+
},
|
|
415
|
+
deploymentReadyOption: {
|
|
416
|
+
actionOnTimeout: 'CONTINUE_DEPLOYMENT',
|
|
417
|
+
waitTimeInMinutes: 0,
|
|
418
|
+
},
|
|
419
|
+
greenFleetProvisioningOption: {
|
|
420
|
+
action: 'COPY_AUTO_SCALING_GROUP',
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Blue/green with delayed termination
|
|
426
|
+
*/
|
|
427
|
+
withDelay: (
|
|
428
|
+
terminationWaitTimeInMinutes: number,
|
|
429
|
+
): CodeDeployDeploymentGroupOptions['blueGreenDeploymentConfiguration'] => ({
|
|
430
|
+
terminateBlueInstancesOnDeploymentSuccess: {
|
|
431
|
+
action: 'TERMINATE',
|
|
432
|
+
terminationWaitTimeInMinutes,
|
|
433
|
+
},
|
|
434
|
+
deploymentReadyOption: {
|
|
435
|
+
actionOnTimeout: 'CONTINUE_DEPLOYMENT',
|
|
436
|
+
waitTimeInMinutes: 0,
|
|
437
|
+
},
|
|
438
|
+
greenFleetProvisioningOption: {
|
|
439
|
+
action: 'COPY_AUTO_SCALING_GROUP',
|
|
440
|
+
},
|
|
441
|
+
}),
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Blue/green with manual approval
|
|
445
|
+
*/
|
|
446
|
+
withManualApproval: (
|
|
447
|
+
waitTimeInMinutes: number,
|
|
448
|
+
): CodeDeployDeploymentGroupOptions['blueGreenDeploymentConfiguration'] => ({
|
|
449
|
+
terminateBlueInstancesOnDeploymentSuccess: {
|
|
450
|
+
action: 'TERMINATE',
|
|
451
|
+
terminationWaitTimeInMinutes: 5,
|
|
452
|
+
},
|
|
453
|
+
deploymentReadyOption: {
|
|
454
|
+
actionOnTimeout: 'STOP_DEPLOYMENT',
|
|
455
|
+
waitTimeInMinutes,
|
|
456
|
+
},
|
|
457
|
+
greenFleetProvisioningOption: {
|
|
458
|
+
action: 'COPY_AUTO_SCALING_GROUP',
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Blue/green keeping old instances
|
|
464
|
+
*/
|
|
465
|
+
keepBlue: (): CodeDeployDeploymentGroupOptions['blueGreenDeploymentConfiguration'] => ({
|
|
466
|
+
terminateBlueInstancesOnDeploymentSuccess: {
|
|
467
|
+
action: 'KEEP_ALIVE',
|
|
468
|
+
},
|
|
469
|
+
deploymentReadyOption: {
|
|
470
|
+
actionOnTimeout: 'CONTINUE_DEPLOYMENT',
|
|
471
|
+
waitTimeInMinutes: 0,
|
|
472
|
+
},
|
|
473
|
+
greenFleetProvisioningOption: {
|
|
474
|
+
action: 'COPY_AUTO_SCALING_GROUP',
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
} as const
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Common use cases
|
|
481
|
+
*/
|
|
482
|
+
static readonly UseCases = {
|
|
483
|
+
/**
|
|
484
|
+
* Create basic EC2 deployment
|
|
485
|
+
*/
|
|
486
|
+
ec2Deployment: (
|
|
487
|
+
slug: string,
|
|
488
|
+
environment: EnvironmentType,
|
|
489
|
+
serviceRoleArn: string,
|
|
490
|
+
autoScalingGroups: string[],
|
|
491
|
+
): {
|
|
492
|
+
application: CodeDeployApplication
|
|
493
|
+
appId: string
|
|
494
|
+
deploymentGroup: CodeDeployDeploymentGroup
|
|
495
|
+
groupId: string
|
|
496
|
+
} => {
|
|
497
|
+
const { application, logicalId: appId } = Deployment.createApplication({
|
|
498
|
+
slug,
|
|
499
|
+
environment,
|
|
500
|
+
computePlatform: 'Server',
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const { deploymentGroup, logicalId: groupId } = Deployment.createDeploymentGroup(appId, {
|
|
504
|
+
slug,
|
|
505
|
+
environment,
|
|
506
|
+
serviceRoleArn,
|
|
507
|
+
autoScalingGroups,
|
|
508
|
+
deploymentConfigName: 'CodeDeployDefault.OneAtATime',
|
|
509
|
+
autoRollbackConfiguration: Deployment.RollbackConfigs.onFailure(),
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
return { application, appId, deploymentGroup, groupId }
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Create Lambda deployment with canary
|
|
517
|
+
*/
|
|
518
|
+
lambdaCanaryDeployment: (
|
|
519
|
+
slug: string,
|
|
520
|
+
environment: EnvironmentType,
|
|
521
|
+
serviceRoleArn: string,
|
|
522
|
+
canaryPercentage: number = 10,
|
|
523
|
+
canaryInterval: number = 5,
|
|
524
|
+
): {
|
|
525
|
+
application: CodeDeployApplication
|
|
526
|
+
appId: string
|
|
527
|
+
deploymentConfig: CodeDeployDeploymentConfig
|
|
528
|
+
configId: string
|
|
529
|
+
deploymentGroup: CodeDeployDeploymentGroup
|
|
530
|
+
groupId: string
|
|
531
|
+
} => {
|
|
532
|
+
const { application, logicalId: appId } = Deployment.createApplication({
|
|
533
|
+
slug,
|
|
534
|
+
environment,
|
|
535
|
+
computePlatform: 'Lambda',
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
const { deploymentConfig, logicalId: configId } = Deployment.createDeploymentConfig({
|
|
539
|
+
slug,
|
|
540
|
+
environment,
|
|
541
|
+
trafficRoutingConfig: Deployment.TrafficRouting.canary(canaryPercentage, canaryInterval),
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const { deploymentGroup, logicalId: groupId } = Deployment.createDeploymentGroup(appId, {
|
|
545
|
+
slug,
|
|
546
|
+
environment,
|
|
547
|
+
serviceRoleArn,
|
|
548
|
+
deploymentConfigName: Fn.Ref(configId) as unknown as string,
|
|
549
|
+
autoRollbackConfiguration: Deployment.RollbackConfigs.onAlarmOrFailure(),
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
return { application, appId, deploymentConfig, configId, deploymentGroup, groupId }
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Create ECS blue/green deployment
|
|
557
|
+
*/
|
|
558
|
+
ecsBlueGreenDeployment: (
|
|
559
|
+
slug: string,
|
|
560
|
+
environment: EnvironmentType,
|
|
561
|
+
serviceRoleArn: string,
|
|
562
|
+
targetGroupName: string,
|
|
563
|
+
): {
|
|
564
|
+
application: CodeDeployApplication
|
|
565
|
+
appId: string
|
|
566
|
+
deploymentGroup: CodeDeployDeploymentGroup
|
|
567
|
+
groupId: string
|
|
568
|
+
} => {
|
|
569
|
+
const { application, logicalId: appId } = Deployment.createApplication({
|
|
570
|
+
slug,
|
|
571
|
+
environment,
|
|
572
|
+
computePlatform: 'ECS',
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const { deploymentGroup, logicalId: groupId } = Deployment.createDeploymentGroup(appId, {
|
|
576
|
+
slug,
|
|
577
|
+
environment,
|
|
578
|
+
serviceRoleArn,
|
|
579
|
+
loadBalancerInfo: {
|
|
580
|
+
targetGroupInfoList: [{ name: targetGroupName }],
|
|
581
|
+
},
|
|
582
|
+
blueGreenDeploymentConfiguration: Deployment.BlueGreenConfigs.standard(),
|
|
583
|
+
autoRollbackConfiguration: Deployment.RollbackConfigs.onFailure(),
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
return { application, appId, deploymentGroup, groupId }
|
|
587
|
+
},
|
|
588
|
+
} as const
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Deployment strategy helpers
|
|
592
|
+
*/
|
|
593
|
+
static readonly Strategies = {
|
|
594
|
+
/**
|
|
595
|
+
* Rolling deployment strategy
|
|
596
|
+
*/
|
|
597
|
+
rolling: (batchPercentage: number = 25): DeploymentStrategyOptions => ({
|
|
598
|
+
type: 'rolling',
|
|
599
|
+
batchPercentage,
|
|
600
|
+
}),
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Blue-green deployment strategy
|
|
604
|
+
*/
|
|
605
|
+
blueGreen: (): DeploymentStrategyOptions => ({
|
|
606
|
+
type: 'blue-green',
|
|
607
|
+
}),
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Canary deployment strategy
|
|
611
|
+
*/
|
|
612
|
+
canary: (canaryPercentage: number = 10, canaryInterval: number = 5): DeploymentStrategyOptions => ({
|
|
613
|
+
type: 'canary',
|
|
614
|
+
canaryPercentage,
|
|
615
|
+
canaryInterval,
|
|
616
|
+
}),
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* All at once deployment strategy
|
|
620
|
+
*/
|
|
621
|
+
allAtOnce: (): DeploymentStrategyOptions => ({
|
|
622
|
+
type: 'all-at-once',
|
|
623
|
+
}),
|
|
624
|
+
} as const
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Asset Hashing Utilities
|
|
629
|
+
* Provides content-based hashing for cache invalidation
|
|
630
|
+
*/
|
|
631
|
+
export class AssetHasher {
|
|
632
|
+
/**
|
|
633
|
+
* Common content types by file extension
|
|
634
|
+
*/
|
|
635
|
+
static readonly ContentTypes: Record<string, string> = {
|
|
636
|
+
'.html': 'text/html',
|
|
637
|
+
'.htm': 'text/html',
|
|
638
|
+
'.css': 'text/css',
|
|
639
|
+
'.js': 'application/javascript',
|
|
640
|
+
'.mjs': 'application/javascript',
|
|
641
|
+
'.json': 'application/json',
|
|
642
|
+
'.xml': 'application/xml',
|
|
643
|
+
'.svg': 'image/svg+xml',
|
|
644
|
+
'.png': 'image/png',
|
|
645
|
+
'.jpg': 'image/jpeg',
|
|
646
|
+
'.jpeg': 'image/jpeg',
|
|
647
|
+
'.gif': 'image/gif',
|
|
648
|
+
'.webp': 'image/webp',
|
|
649
|
+
'.avif': 'image/avif',
|
|
650
|
+
'.ico': 'image/x-icon',
|
|
651
|
+
'.woff': 'font/woff',
|
|
652
|
+
'.woff2': 'font/woff2',
|
|
653
|
+
'.ttf': 'font/ttf',
|
|
654
|
+
'.otf': 'font/otf',
|
|
655
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
656
|
+
'.pdf': 'application/pdf',
|
|
657
|
+
'.txt': 'text/plain',
|
|
658
|
+
'.md': 'text/markdown',
|
|
659
|
+
'.map': 'application/json',
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Files that should NOT be hashed (typically entry points)
|
|
664
|
+
*/
|
|
665
|
+
static readonly NoHashPatterns: RegExp[] = [
|
|
666
|
+
/^index\.html$/,
|
|
667
|
+
/^favicon\.ico$/,
|
|
668
|
+
/^robots\.txt$/,
|
|
669
|
+
/^sitemap\.xml$/,
|
|
670
|
+
/^manifest\.json$/,
|
|
671
|
+
/^\.well-known\//,
|
|
672
|
+
/^_redirects$/,
|
|
673
|
+
/^_headers$/,
|
|
674
|
+
]
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Compute hash for a file's contents
|
|
678
|
+
*/
|
|
679
|
+
static computeFileHash(filePath: string, algorithm: 'md5' | 'sha256' | 'sha1' = 'md5'): string {
|
|
680
|
+
const content = readFileSync(filePath)
|
|
681
|
+
return createHash(algorithm).update(content).digest('hex')
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Compute short hash (first 8 characters)
|
|
686
|
+
*/
|
|
687
|
+
static computeShortHash(filePath: string): string {
|
|
688
|
+
return AssetHasher.computeFileHash(filePath).slice(0, 8)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Get content type for a file
|
|
693
|
+
*/
|
|
694
|
+
static getContentType(filePath: string): string {
|
|
695
|
+
const ext = extname(filePath).toLowerCase()
|
|
696
|
+
return AssetHasher.ContentTypes[ext] || 'application/octet-stream'
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Check if a file should be hashed
|
|
701
|
+
*/
|
|
702
|
+
static shouldHashFile(relativePath: string, customNoHashPatterns?: RegExp[]): boolean {
|
|
703
|
+
const patterns = [...AssetHasher.NoHashPatterns, ...(customNoHashPatterns || [])]
|
|
704
|
+
return !patterns.some(pattern => pattern.test(relativePath))
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Generate a hashed filename
|
|
709
|
+
* e.g., "styles.css" -> "styles.a1b2c3d4.css"
|
|
710
|
+
*/
|
|
711
|
+
static generateHashedFilename(filePath: string, hash: string): string {
|
|
712
|
+
const ext = extname(filePath)
|
|
713
|
+
const name = basename(filePath, ext)
|
|
714
|
+
const dir = dirname(filePath)
|
|
715
|
+
|
|
716
|
+
if (dir === '.') {
|
|
717
|
+
return `${name}.${hash}${ext}`
|
|
718
|
+
}
|
|
719
|
+
return join(dir, `${name}.${hash}${ext}`)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Collect all files in a directory recursively
|
|
724
|
+
*/
|
|
725
|
+
static collectFiles(directory: string, relativeTo?: string): string[] {
|
|
726
|
+
const files: string[] = []
|
|
727
|
+
const baseDir = relativeTo || directory
|
|
728
|
+
|
|
729
|
+
if (!existsSync(directory)) {
|
|
730
|
+
return files
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const entries = readdirSync(directory, { withFileTypes: true })
|
|
734
|
+
|
|
735
|
+
for (const entry of entries) {
|
|
736
|
+
const fullPath = join(directory, entry.name)
|
|
737
|
+
|
|
738
|
+
if (entry.isDirectory()) {
|
|
739
|
+
files.push(...AssetHasher.collectFiles(fullPath, baseDir))
|
|
740
|
+
}
|
|
741
|
+
else if (entry.isFile()) {
|
|
742
|
+
files.push(fullPath)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return files
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Hash all assets in a directory
|
|
751
|
+
*/
|
|
752
|
+
static hashDirectory(options: {
|
|
753
|
+
sourceDir: string
|
|
754
|
+
outputDir?: string
|
|
755
|
+
excludePatterns?: RegExp[]
|
|
756
|
+
hashAlgorithm?: 'md5' | 'sha256' | 'sha1'
|
|
757
|
+
copyUnhashed?: boolean
|
|
758
|
+
}): AssetManifest {
|
|
759
|
+
const {
|
|
760
|
+
sourceDir,
|
|
761
|
+
outputDir,
|
|
762
|
+
excludePatterns = [],
|
|
763
|
+
hashAlgorithm = 'md5',
|
|
764
|
+
copyUnhashed = true,
|
|
765
|
+
} = options
|
|
766
|
+
|
|
767
|
+
const files = AssetHasher.collectFiles(sourceDir)
|
|
768
|
+
const assets: HashedAsset[] = []
|
|
769
|
+
const hashMap: Record<string, string> = {}
|
|
770
|
+
|
|
771
|
+
for (const filePath of files) {
|
|
772
|
+
const relativePath = filePath.replace(sourceDir, '').replace(/^[/\\]/, '')
|
|
773
|
+
const shouldHash = AssetHasher.shouldHashFile(relativePath, excludePatterns)
|
|
774
|
+
const stats = statSync(filePath)
|
|
775
|
+
const hash = shouldHash ? AssetHasher.computeFileHash(filePath, hashAlgorithm).slice(0, 8) : ''
|
|
776
|
+
const hashedRelativePath = shouldHash
|
|
777
|
+
? AssetHasher.generateHashedFilename(relativePath, hash)
|
|
778
|
+
: relativePath
|
|
779
|
+
|
|
780
|
+
const asset: HashedAsset = {
|
|
781
|
+
originalPath: relativePath,
|
|
782
|
+
hashedPath: hashedRelativePath,
|
|
783
|
+
hash,
|
|
784
|
+
size: stats.size,
|
|
785
|
+
contentType: AssetHasher.getContentType(filePath),
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
assets.push(asset)
|
|
789
|
+
hashMap[relativePath] = hashedRelativePath
|
|
790
|
+
|
|
791
|
+
// Copy to output directory if specified
|
|
792
|
+
if (outputDir) {
|
|
793
|
+
const destPath = join(outputDir, hashedRelativePath)
|
|
794
|
+
const destDir = dirname(destPath)
|
|
795
|
+
|
|
796
|
+
// Ensure destination directory exists
|
|
797
|
+
if (!existsSync(destDir)) {
|
|
798
|
+
const { mkdirSync } = require('node:fs')
|
|
799
|
+
mkdirSync(destDir, { recursive: true })
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (shouldHash || copyUnhashed) {
|
|
803
|
+
copyFileSync(filePath, destPath)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const manifest: AssetManifest = {
|
|
809
|
+
version: '1.0',
|
|
810
|
+
timestamp: new Date().toISOString(),
|
|
811
|
+
assets,
|
|
812
|
+
hashMap,
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Write manifest to output directory
|
|
816
|
+
if (outputDir) {
|
|
817
|
+
const manifestPath = join(outputDir, 'asset-manifest.json')
|
|
818
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return manifest
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Get paths that need CloudFront invalidation
|
|
826
|
+
* Compares old and new manifests to find changed files
|
|
827
|
+
*/
|
|
828
|
+
static getInvalidationPaths(
|
|
829
|
+
oldManifest: AssetManifest | null,
|
|
830
|
+
newManifest: AssetManifest,
|
|
831
|
+
): string[] {
|
|
832
|
+
const invalidationPaths: string[] = []
|
|
833
|
+
|
|
834
|
+
if (!oldManifest) {
|
|
835
|
+
// If no old manifest, invalidate everything
|
|
836
|
+
return ['/*']
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const oldHashMap = oldManifest.hashMap
|
|
840
|
+
const newHashMap = newManifest.hashMap
|
|
841
|
+
|
|
842
|
+
// Find changed and new files
|
|
843
|
+
for (const [originalPath, newHashedPath] of Object.entries(newHashMap)) {
|
|
844
|
+
const oldHashedPath = oldHashMap[originalPath]
|
|
845
|
+
|
|
846
|
+
if (!oldHashedPath || oldHashedPath !== newHashedPath) {
|
|
847
|
+
// File is new or changed
|
|
848
|
+
invalidationPaths.push(`/${originalPath}`)
|
|
849
|
+
if (oldHashedPath && oldHashedPath !== newHashedPath) {
|
|
850
|
+
// Also invalidate old hashed path
|
|
851
|
+
invalidationPaths.push(`/${oldHashedPath}`)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Find deleted files
|
|
857
|
+
for (const [originalPath, oldHashedPath] of Object.entries(oldHashMap)) {
|
|
858
|
+
if (!newHashMap[originalPath]) {
|
|
859
|
+
invalidationPaths.push(`/${originalPath}`)
|
|
860
|
+
invalidationPaths.push(`/${oldHashedPath}`)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// If too many paths, just invalidate everything
|
|
865
|
+
if (invalidationPaths.length > 100) {
|
|
866
|
+
return ['/*']
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return [...new Set(invalidationPaths)] // Remove duplicates
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Update HTML files to reference hashed assets
|
|
874
|
+
*/
|
|
875
|
+
static updateHtmlReferences(options: {
|
|
876
|
+
htmlDir: string
|
|
877
|
+
manifest: AssetManifest
|
|
878
|
+
basePath?: string
|
|
879
|
+
}): void {
|
|
880
|
+
const { htmlDir, manifest, basePath = '' } = options
|
|
881
|
+
|
|
882
|
+
const htmlFiles = AssetHasher.collectFiles(htmlDir)
|
|
883
|
+
.filter(f => f.endsWith('.html') || f.endsWith('.htm'))
|
|
884
|
+
|
|
885
|
+
for (const htmlFile of htmlFiles) {
|
|
886
|
+
let content = readFileSync(htmlFile, 'utf-8')
|
|
887
|
+
|
|
888
|
+
// Replace references to original paths with hashed paths
|
|
889
|
+
for (const [originalPath, hashedPath] of Object.entries(manifest.hashMap)) {
|
|
890
|
+
if (originalPath === hashedPath) continue // Skip unhashed files
|
|
891
|
+
|
|
892
|
+
// Handle various reference formats
|
|
893
|
+
const patterns = [
|
|
894
|
+
// src="path" or href="path"
|
|
895
|
+
new RegExp(`(src|href)=["']${basePath}/?${AssetHasher.escapeRegExp(originalPath)}["']`, 'g'),
|
|
896
|
+
// url(path)
|
|
897
|
+
new RegExp(`url\\(["']?${basePath}/?${AssetHasher.escapeRegExp(originalPath)}["']?\\)`, 'g'),
|
|
898
|
+
]
|
|
899
|
+
|
|
900
|
+
for (const pattern of patterns) {
|
|
901
|
+
content = content.replace(pattern, (match) => {
|
|
902
|
+
return match.replace(originalPath, hashedPath)
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
writeFileSync(htmlFile, content)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Update CSS files to reference hashed assets
|
|
913
|
+
*/
|
|
914
|
+
static updateCssReferences(options: {
|
|
915
|
+
cssDir: string
|
|
916
|
+
manifest: AssetManifest
|
|
917
|
+
basePath?: string
|
|
918
|
+
}): void {
|
|
919
|
+
const { cssDir, manifest, basePath = '' } = options
|
|
920
|
+
|
|
921
|
+
const cssFiles = AssetHasher.collectFiles(cssDir)
|
|
922
|
+
.filter(f => f.endsWith('.css'))
|
|
923
|
+
|
|
924
|
+
for (const cssFile of cssFiles) {
|
|
925
|
+
let content = readFileSync(cssFile, 'utf-8')
|
|
926
|
+
|
|
927
|
+
// Replace url() references
|
|
928
|
+
for (const [originalPath, hashedPath] of Object.entries(manifest.hashMap)) {
|
|
929
|
+
if (originalPath === hashedPath) continue
|
|
930
|
+
|
|
931
|
+
const pattern = new RegExp(
|
|
932
|
+
`url\\(["']?${basePath}/?${AssetHasher.escapeRegExp(originalPath)}["']?\\)`,
|
|
933
|
+
'g',
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
content = content.replace(pattern, `url(${basePath}/${hashedPath})`)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
writeFileSync(cssFile, content)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Escape special regex characters
|
|
945
|
+
*/
|
|
946
|
+
private static escapeRegExp(string: string): string {
|
|
947
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Generate a deployment manifest for S3
|
|
952
|
+
*/
|
|
953
|
+
static generateS3DeploymentManifest(options: {
|
|
954
|
+
sourceDir: string
|
|
955
|
+
bucketName: string
|
|
956
|
+
keyPrefix?: string
|
|
957
|
+
excludePatterns?: RegExp[]
|
|
958
|
+
cacheControl?: {
|
|
959
|
+
hashed?: string
|
|
960
|
+
unhashed?: string
|
|
961
|
+
html?: string
|
|
962
|
+
}
|
|
963
|
+
}): Array<{
|
|
964
|
+
localPath: string
|
|
965
|
+
s3Key: string
|
|
966
|
+
contentType: string
|
|
967
|
+
cacheControl: string
|
|
968
|
+
hash: string
|
|
969
|
+
}> {
|
|
970
|
+
const {
|
|
971
|
+
sourceDir,
|
|
972
|
+
keyPrefix = '',
|
|
973
|
+
excludePatterns = [],
|
|
974
|
+
cacheControl = {
|
|
975
|
+
hashed: 'public, max-age=31536000, immutable', // 1 year for hashed files
|
|
976
|
+
unhashed: 'public, max-age=3600', // 1 hour for unhashed
|
|
977
|
+
html: 'public, max-age=0, must-revalidate', // Always revalidate HTML
|
|
978
|
+
},
|
|
979
|
+
} = options
|
|
980
|
+
|
|
981
|
+
const manifest = AssetHasher.hashDirectory({
|
|
982
|
+
sourceDir,
|
|
983
|
+
excludePatterns,
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
return manifest.assets.map((asset) => {
|
|
987
|
+
const isHashed = asset.hash !== ''
|
|
988
|
+
const isHtml = asset.contentType === 'text/html'
|
|
989
|
+
|
|
990
|
+
let cc = cacheControl.unhashed || 'public, max-age=3600'
|
|
991
|
+
if (isHtml) {
|
|
992
|
+
cc = cacheControl.html || 'public, max-age=0, must-revalidate'
|
|
993
|
+
}
|
|
994
|
+
else if (isHashed) {
|
|
995
|
+
cc = cacheControl.hashed || 'public, max-age=31536000, immutable'
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
localPath: join(sourceDir, asset.originalPath),
|
|
1000
|
+
s3Key: keyPrefix ? `${keyPrefix}/${asset.hashedPath}` : asset.hashedPath,
|
|
1001
|
+
contentType: asset.contentType,
|
|
1002
|
+
cacheControl: cc,
|
|
1003
|
+
hash: asset.hash,
|
|
1004
|
+
}
|
|
1005
|
+
})
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Compare two asset manifests to detect changes
|
|
1010
|
+
*/
|
|
1011
|
+
static compareManifests(oldManifest: AssetManifest, newManifest: AssetManifest): {
|
|
1012
|
+
added: string[]
|
|
1013
|
+
removed: string[]
|
|
1014
|
+
changed: string[]
|
|
1015
|
+
unchanged: string[]
|
|
1016
|
+
} {
|
|
1017
|
+
const result = {
|
|
1018
|
+
added: [] as string[],
|
|
1019
|
+
removed: [] as string[],
|
|
1020
|
+
changed: [] as string[],
|
|
1021
|
+
unchanged: [] as string[],
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const oldPaths = new Set(Object.keys(oldManifest.hashMap))
|
|
1025
|
+
const newPaths = new Set(Object.keys(newManifest.hashMap))
|
|
1026
|
+
|
|
1027
|
+
// Find added files
|
|
1028
|
+
for (const path of newPaths) {
|
|
1029
|
+
if (!oldPaths.has(path)) {
|
|
1030
|
+
result.added.push(path)
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Find removed files
|
|
1035
|
+
for (const path of oldPaths) {
|
|
1036
|
+
if (!newPaths.has(path)) {
|
|
1037
|
+
result.removed.push(path)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Find changed and unchanged files
|
|
1042
|
+
for (const path of newPaths) {
|
|
1043
|
+
if (oldPaths.has(path)) {
|
|
1044
|
+
if (oldManifest.hashMap[path] !== newManifest.hashMap[path]) {
|
|
1045
|
+
result.changed.push(path)
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
result.unchanged.push(path)
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return result
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Load an existing asset manifest from a file
|
|
1058
|
+
*/
|
|
1059
|
+
static loadManifest(manifestPath: string): AssetManifest | null {
|
|
1060
|
+
if (!existsSync(manifestPath)) {
|
|
1061
|
+
return null
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
const content = readFileSync(manifestPath, 'utf-8')
|
|
1066
|
+
return JSON.parse(content) as AssetManifest
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return null
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Save an asset manifest to a file
|
|
1075
|
+
*/
|
|
1076
|
+
static saveManifest(manifest: AssetManifest, manifestPath: string): void {
|
|
1077
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
|
1078
|
+
}
|
|
1079
|
+
}
|