@fnd-platform/cli 1.0.0-alpha.7 → 1.0.0-alpha.9
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/lib/commands/add.d.ts.map +1 -1
- package/lib/commands/add.js +31 -6
- package/lib/commands/add.js.map +1 -1
- package/lib/commands/deploy.d.ts +22 -5
- package/lib/commands/deploy.d.ts.map +1 -1
- package/lib/commands/deploy.js +170 -15
- package/lib/commands/deploy.js.map +1 -1
- package/lib/commands/destroy.d.ts +69 -0
- package/lib/commands/destroy.d.ts.map +1 -0
- package/lib/commands/destroy.js +177 -0
- package/lib/commands/destroy.js.map +1 -0
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.d.ts.map +1 -1
- package/lib/commands/index.js +3 -1
- package/lib/commands/index.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/lib/deploy-utils.d.ts +186 -0
- package/lib/lib/deploy-utils.d.ts.map +1 -0
- package/lib/lib/deploy-utils.js +367 -0
- package/lib/lib/deploy-utils.js.map +1 -0
- package/lib/lib/infra-generator.d.ts +108 -0
- package/lib/lib/infra-generator.d.ts.map +1 -1
- package/lib/lib/infra-generator.js +911 -39
- package/lib/lib/infra-generator.js.map +1 -1
- package/package.json +14 -14
- package/LICENSE +0 -21
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateInfraPackage = generateInfraPackage;
|
|
4
|
+
exports.getFrontendStackTemplate = getFrontendStackTemplate;
|
|
5
|
+
exports.getCmsStackTemplate = getCmsStackTemplate;
|
|
6
|
+
exports.updateAppTsForStack = updateAppTsForStack;
|
|
7
|
+
exports.generateFrontendStack = generateFrontendStack;
|
|
8
|
+
exports.generateCmsStack = generateCmsStack;
|
|
4
9
|
const fs_1 = require("fs");
|
|
5
10
|
const path_1 = require("path");
|
|
6
11
|
const logger_js_1 = require("./logger.js");
|
|
@@ -20,13 +25,18 @@ function generateInfraPackage(options) {
|
|
|
20
25
|
logger_js_1.logger.info('Generating infrastructure package...');
|
|
21
26
|
// Create directory structure
|
|
22
27
|
(0, fs_1.mkdirSync)((0, path_1.join)(infraDir, 'src/stacks'), { recursive: true });
|
|
23
|
-
// Generate files
|
|
28
|
+
// Generate package config files
|
|
24
29
|
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'package.json'), getPackageJsonTemplate(options));
|
|
25
30
|
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'tsconfig.json'), getTsconfigTemplate());
|
|
26
31
|
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'cdk.json'), getCdkJsonTemplate());
|
|
32
|
+
// Generate app entry point
|
|
27
33
|
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'src/app.ts'), getAppTemplate(options));
|
|
34
|
+
// Generate stack files
|
|
35
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'src/stacks/database-stack.ts'), getDatabaseStackTemplate(options));
|
|
36
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'src/stacks/media-stack.ts'), getMediaStackTemplate(options));
|
|
28
37
|
(0, fs_1.writeFileSync)((0, path_1.join)(infraDir, 'src/stacks/api-stack.ts'), getApiStackTemplate(options));
|
|
29
38
|
logger_js_1.logger.success('Infrastructure package generated at packages/infra');
|
|
39
|
+
logger_js_1.logger.info('Generated stacks: DatabaseStack, MediaStack, ApiStack');
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
42
|
* Returns the package.json template.
|
|
@@ -48,10 +58,13 @@ function getPackageJsonTemplate(options) {
|
|
|
48
58
|
'@fnd-platform/constructs': '^1.0.0-alpha.1',
|
|
49
59
|
'aws-cdk-lib': '^2.130.0',
|
|
50
60
|
constructs: '^10.3.0',
|
|
61
|
+
'source-map-support': '^0.5.21',
|
|
51
62
|
},
|
|
52
63
|
devDependencies: {
|
|
53
64
|
'@types/node': '^20.0.0',
|
|
65
|
+
'@types/source-map-support': '^0.5.10',
|
|
54
66
|
'aws-cdk': '^2.130.0',
|
|
67
|
+
esbuild: '^0.20.0',
|
|
55
68
|
typescript: '^5.6.3',
|
|
56
69
|
},
|
|
57
70
|
}, null, 2);
|
|
@@ -103,80 +116,233 @@ function getCdkJsonTemplate() {
|
|
|
103
116
|
* Returns the app.ts template.
|
|
104
117
|
*/
|
|
105
118
|
function getAppTemplate(options) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
const pascalName = toPascalCase(options.projectName);
|
|
120
|
+
return `#!/usr/bin/env node
|
|
121
|
+
/**
|
|
122
|
+
* CDK Application Entry Point for ${options.projectName}
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
import 'source-map-support/register';
|
|
126
|
+
import * as cdk from 'aws-cdk-lib';
|
|
127
|
+
|
|
128
|
+
import { DatabaseStack } from './stacks/database-stack';
|
|
129
|
+
import { MediaStack } from './stacks/media-stack';
|
|
130
|
+
import { ApiStack } from './stacks/api-stack';
|
|
111
131
|
|
|
112
|
-
|
|
132
|
+
// Create the CDK app
|
|
133
|
+
const app = new cdk.App();
|
|
113
134
|
|
|
114
|
-
// Get stage from context (default
|
|
135
|
+
// Get deployment stage from context (default: dev)
|
|
115
136
|
const stage = app.node.tryGetContext('stage') || 'dev';
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
// Validate stage
|
|
139
|
+
const validStages = ['dev', 'staging', 'prod'];
|
|
140
|
+
if (!validStages.includes(stage)) {
|
|
141
|
+
throw new Error(\`Invalid stage '\${stage}'. Must be one of: \${validStages.join(', ')}\`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Environment configuration
|
|
145
|
+
const env: cdk.Environment = {
|
|
146
|
+
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
147
|
+
region: process.env.CDK_DEFAULT_REGION || 'us-east-1',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Stack naming convention
|
|
151
|
+
const stackName = (name: string) => \`${pascalName}\${name}Stack-\${stage}\`;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Database Stack
|
|
155
|
+
* Creates DynamoDB table with GSIs
|
|
156
|
+
*/
|
|
157
|
+
const databaseStack = new DatabaseStack(app, stackName('Database'), {
|
|
158
|
+
env,
|
|
159
|
+
stage,
|
|
160
|
+
description: '${options.projectName}: DynamoDB table',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Media Stack
|
|
165
|
+
* Creates S3 bucket and CloudFront for media
|
|
166
|
+
*/
|
|
167
|
+
const mediaStack = new MediaStack(app, stackName('Media'), {
|
|
168
|
+
env,
|
|
169
|
+
stage,
|
|
170
|
+
description: '${options.projectName}: Media storage',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* API Stack
|
|
175
|
+
* Creates Lambda functions and API Gateway
|
|
176
|
+
*/
|
|
177
|
+
const apiStack = new ApiStack(app, stackName('Api'), {
|
|
178
|
+
env,
|
|
118
179
|
stage,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
180
|
+
table: databaseStack.table,
|
|
181
|
+
mediaBucket: mediaStack.bucket,
|
|
182
|
+
description: '${options.projectName}: API',
|
|
123
183
|
});
|
|
124
184
|
|
|
125
|
-
|
|
185
|
+
// API depends on database and media
|
|
186
|
+
apiStack.addDependency(databaseStack);
|
|
187
|
+
apiStack.addDependency(mediaStack);
|
|
188
|
+
|
|
189
|
+
// Add tags to all stacks
|
|
190
|
+
const tags = {
|
|
191
|
+
Project: '${options.projectName}',
|
|
192
|
+
Stage: stage,
|
|
193
|
+
ManagedBy: 'cdk',
|
|
194
|
+
CreatedBy: 'fnd-platform',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
Object.entries(tags).forEach(([key, value]) => {
|
|
198
|
+
cdk.Tags.of(app).add(key, value);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Output summary
|
|
202
|
+
console.log(\`
|
|
203
|
+
╔════════════════════════════════════════════════════════════╗
|
|
204
|
+
║ ${options.projectName} Infrastructure
|
|
205
|
+
╠════════════════════════════════════════════════════════════╣
|
|
206
|
+
║ Stage: \${stage}
|
|
207
|
+
║ Region: \${env.region || 'us-east-1'}
|
|
208
|
+
║
|
|
209
|
+
║ Stacks:
|
|
210
|
+
║ - \${stackName('Database')}
|
|
211
|
+
║ - \${stackName('Media')}
|
|
212
|
+
║ - \${stackName('Api')}
|
|
213
|
+
╚════════════════════════════════════════════════════════════╝
|
|
214
|
+
\`);
|
|
126
215
|
`;
|
|
127
216
|
}
|
|
128
217
|
/**
|
|
129
218
|
* Returns the api-stack.ts template.
|
|
130
219
|
*/
|
|
131
220
|
function getApiStackTemplate(options) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
221
|
+
const pascalName = toPascalCase(options.projectName);
|
|
222
|
+
const lowerName = options.projectName.toLowerCase();
|
|
223
|
+
return `/**
|
|
224
|
+
* API Stack for ${options.projectName}
|
|
225
|
+
*
|
|
226
|
+
* Creates Lambda functions and API Gateway with content CRUD endpoints.
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
import * as cdk from 'aws-cdk-lib';
|
|
230
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
231
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
232
|
+
import * as path from 'path';
|
|
233
|
+
import { Construct } from 'constructs';
|
|
234
|
+
import {
|
|
136
235
|
FndLambdaFunction,
|
|
137
236
|
FndApiGateway,
|
|
138
237
|
} from '@fnd-platform/constructs';
|
|
139
|
-
import { Construct } from 'constructs';
|
|
140
|
-
import * as path from 'path';
|
|
141
238
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
239
|
+
export interface ApiStackProps extends cdk.StackProps {
|
|
240
|
+
stage: string;
|
|
241
|
+
table: dynamodb.ITable;
|
|
242
|
+
mediaBucket: s3.IBucket;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class ApiStack extends cdk.Stack {
|
|
246
|
+
/**
|
|
247
|
+
* The API URL
|
|
248
|
+
*/
|
|
249
|
+
public readonly apiUrl: string;
|
|
250
|
+
|
|
251
|
+
constructor(scope: Construct, id: string, props: ApiStackProps) {
|
|
151
252
|
super(scope, id, props);
|
|
152
253
|
|
|
153
|
-
|
|
154
|
-
const apiPackagePath = path.resolve(__dirname, '../../${options.apiPackageName}');
|
|
254
|
+
const { stage, table, mediaBucket } = props;
|
|
155
255
|
|
|
156
256
|
// Health check handler
|
|
157
257
|
const healthHandler = new FndLambdaFunction(this, 'HealthHandler', {
|
|
158
|
-
entry: path.join(
|
|
159
|
-
stage
|
|
258
|
+
entry: path.join(__dirname, '../../../${options.apiPackageName}/src/handlers/health.ts'),
|
|
259
|
+
stage,
|
|
160
260
|
description: 'Health check endpoint',
|
|
261
|
+
environment: {
|
|
262
|
+
TABLE_NAME: table.tableName,
|
|
263
|
+
MEDIA_BUCKET: mediaBucket.bucketName,
|
|
264
|
+
},
|
|
161
265
|
});
|
|
162
266
|
|
|
163
|
-
//
|
|
267
|
+
// Content CRUD handler (single handler for all content operations)
|
|
268
|
+
const contentHandler = new FndLambdaFunction(this, 'ContentHandler', {
|
|
269
|
+
entry: path.join(__dirname, '../../../${options.apiPackageName}/src/handlers/content.ts'),
|
|
270
|
+
stage,
|
|
271
|
+
description: 'Content CRUD operations',
|
|
272
|
+
environment: {
|
|
273
|
+
TABLE_NAME: table.tableName,
|
|
274
|
+
MEDIA_BUCKET: mediaBucket.bucketName,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Grant DynamoDB access to handlers
|
|
279
|
+
table.grantReadWriteData(healthHandler.function);
|
|
280
|
+
table.grantReadWriteData(contentHandler.function);
|
|
281
|
+
|
|
282
|
+
// Grant S3 access for media operations
|
|
283
|
+
mediaBucket.grantReadWrite(healthHandler.function);
|
|
284
|
+
mediaBucket.grantReadWrite(contentHandler.function);
|
|
285
|
+
|
|
286
|
+
// API Gateway with all routes
|
|
164
287
|
const api = new FndApiGateway(this, 'Api', {
|
|
165
|
-
name: '${
|
|
166
|
-
stage
|
|
288
|
+
name: '${lowerName}-api',
|
|
289
|
+
stage,
|
|
167
290
|
routes: [
|
|
291
|
+
// Health check (public)
|
|
168
292
|
{
|
|
169
293
|
method: 'GET',
|
|
170
294
|
path: '/health',
|
|
171
295
|
handler: healthHandler.function,
|
|
172
296
|
requiresAuth: false,
|
|
173
297
|
},
|
|
174
|
-
//
|
|
298
|
+
// Content routes
|
|
299
|
+
{
|
|
300
|
+
method: 'GET',
|
|
301
|
+
path: '/content',
|
|
302
|
+
handler: contentHandler.function,
|
|
303
|
+
requiresAuth: false,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
method: 'POST',
|
|
307
|
+
path: '/content',
|
|
308
|
+
handler: contentHandler.function,
|
|
309
|
+
requiresAuth: true,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
method: 'GET',
|
|
313
|
+
path: '/content/{id}',
|
|
314
|
+
handler: contentHandler.function,
|
|
315
|
+
requiresAuth: false,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
method: 'PUT',
|
|
319
|
+
path: '/content/{id}',
|
|
320
|
+
handler: contentHandler.function,
|
|
321
|
+
requiresAuth: true,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
method: 'DELETE',
|
|
325
|
+
path: '/content/{id}',
|
|
326
|
+
handler: contentHandler.function,
|
|
327
|
+
requiresAuth: true,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
method: 'GET',
|
|
331
|
+
path: '/content/slug/{slug}',
|
|
332
|
+
handler: contentHandler.function,
|
|
333
|
+
requiresAuth: false,
|
|
334
|
+
},
|
|
175
335
|
],
|
|
176
336
|
});
|
|
177
337
|
|
|
178
|
-
|
|
179
|
-
|
|
338
|
+
this.apiUrl = api.url;
|
|
339
|
+
|
|
340
|
+
// Outputs
|
|
341
|
+
new cdk.CfnOutput(this, 'ApiUrl', {
|
|
342
|
+
value: api.url,
|
|
343
|
+
description: 'API Gateway URL',
|
|
344
|
+
exportName: \`${pascalName}-ApiUrl-\${stage}\`,
|
|
345
|
+
});
|
|
180
346
|
}
|
|
181
347
|
}
|
|
182
348
|
`;
|
|
@@ -190,4 +356,710 @@ function toPascalCase(str) {
|
|
|
190
356
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
191
357
|
.join('');
|
|
192
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Returns the database-stack.ts template.
|
|
361
|
+
*/
|
|
362
|
+
function getDatabaseStackTemplate(options) {
|
|
363
|
+
const pascalName = toPascalCase(options.projectName);
|
|
364
|
+
const lowerName = options.projectName.toLowerCase();
|
|
365
|
+
return `/**
|
|
366
|
+
* Database Stack for ${options.projectName}
|
|
367
|
+
*
|
|
368
|
+
* Creates DynamoDB table with GSIs for single-table design.
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
import * as cdk from 'aws-cdk-lib';
|
|
372
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
373
|
+
import { Construct } from 'constructs';
|
|
374
|
+
|
|
375
|
+
export interface DatabaseStackProps extends cdk.StackProps {
|
|
376
|
+
stage: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export class DatabaseStack extends cdk.Stack {
|
|
380
|
+
/**
|
|
381
|
+
* The main DynamoDB table
|
|
382
|
+
*/
|
|
383
|
+
public readonly table: dynamodb.Table;
|
|
384
|
+
|
|
385
|
+
constructor(scope: Construct, id: string, props: DatabaseStackProps) {
|
|
386
|
+
super(scope, id, props);
|
|
387
|
+
|
|
388
|
+
const { stage } = props;
|
|
389
|
+
const isProd = stage === 'prod';
|
|
390
|
+
|
|
391
|
+
// Create the main table with single-table design
|
|
392
|
+
this.table = new dynamodb.Table(this, 'Table', {
|
|
393
|
+
tableName: \`${lowerName}-\${stage}\`,
|
|
394
|
+
partitionKey: {
|
|
395
|
+
name: 'PK',
|
|
396
|
+
type: dynamodb.AttributeType.STRING,
|
|
397
|
+
},
|
|
398
|
+
sortKey: {
|
|
399
|
+
name: 'SK',
|
|
400
|
+
type: dynamodb.AttributeType.STRING,
|
|
401
|
+
},
|
|
402
|
+
billingMode: isProd
|
|
403
|
+
? dynamodb.BillingMode.PROVISIONED
|
|
404
|
+
: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
405
|
+
...(isProd && {
|
|
406
|
+
readCapacity: 5,
|
|
407
|
+
writeCapacity: 5,
|
|
408
|
+
}),
|
|
409
|
+
removalPolicy: isProd
|
|
410
|
+
? cdk.RemovalPolicy.RETAIN
|
|
411
|
+
: cdk.RemovalPolicy.DESTROY,
|
|
412
|
+
pointInTimeRecovery: isProd,
|
|
413
|
+
encryption: dynamodb.TableEncryption.AWS_MANAGED,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// GSI1: Lookup by slug or secondary key
|
|
417
|
+
this.table.addGlobalSecondaryIndex({
|
|
418
|
+
indexName: 'GSI1',
|
|
419
|
+
partitionKey: {
|
|
420
|
+
name: 'GSI1PK',
|
|
421
|
+
type: dynamodb.AttributeType.STRING,
|
|
422
|
+
},
|
|
423
|
+
sortKey: {
|
|
424
|
+
name: 'GSI1SK',
|
|
425
|
+
type: dynamodb.AttributeType.STRING,
|
|
426
|
+
},
|
|
427
|
+
projectionType: dynamodb.ProjectionType.ALL,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// GSI2: List by type/status with timestamp sorting
|
|
431
|
+
this.table.addGlobalSecondaryIndex({
|
|
432
|
+
indexName: 'GSI2',
|
|
433
|
+
partitionKey: {
|
|
434
|
+
name: 'GSI2PK',
|
|
435
|
+
type: dynamodb.AttributeType.STRING,
|
|
436
|
+
},
|
|
437
|
+
sortKey: {
|
|
438
|
+
name: 'GSI2SK',
|
|
439
|
+
type: dynamodb.AttributeType.STRING,
|
|
440
|
+
},
|
|
441
|
+
projectionType: dynamodb.ProjectionType.ALL,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Enable auto-scaling for production
|
|
445
|
+
if (isProd) {
|
|
446
|
+
const readScaling = this.table.autoScaleReadCapacity({
|
|
447
|
+
minCapacity: 5,
|
|
448
|
+
maxCapacity: 100,
|
|
449
|
+
});
|
|
450
|
+
readScaling.scaleOnUtilization({
|
|
451
|
+
targetUtilizationPercent: 70,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const writeScaling = this.table.autoScaleWriteCapacity({
|
|
455
|
+
minCapacity: 5,
|
|
456
|
+
maxCapacity: 100,
|
|
457
|
+
});
|
|
458
|
+
writeScaling.scaleOnUtilization({
|
|
459
|
+
targetUtilizationPercent: 70,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Outputs
|
|
464
|
+
new cdk.CfnOutput(this, 'TableName', {
|
|
465
|
+
value: this.table.tableName,
|
|
466
|
+
description: 'DynamoDB table name',
|
|
467
|
+
exportName: \`${pascalName}-TableName-\${stage}\`,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
new cdk.CfnOutput(this, 'TableArn', {
|
|
471
|
+
value: this.table.tableArn,
|
|
472
|
+
description: 'DynamoDB table ARN',
|
|
473
|
+
exportName: \`${pascalName}-TableArn-\${stage}\`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Returns the media-stack.ts template.
|
|
481
|
+
*/
|
|
482
|
+
function getMediaStackTemplate(options) {
|
|
483
|
+
const pascalName = toPascalCase(options.projectName);
|
|
484
|
+
const lowerName = options.projectName.toLowerCase();
|
|
485
|
+
return `/**
|
|
486
|
+
* Media Stack for ${options.projectName}
|
|
487
|
+
*
|
|
488
|
+
* Creates S3 bucket and CloudFront distribution for media uploads.
|
|
489
|
+
*/
|
|
490
|
+
|
|
491
|
+
import * as cdk from 'aws-cdk-lib';
|
|
492
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
493
|
+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
|
494
|
+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
495
|
+
import { Construct } from 'constructs';
|
|
496
|
+
|
|
497
|
+
export interface MediaStackProps extends cdk.StackProps {
|
|
498
|
+
stage: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export class MediaStack extends cdk.Stack {
|
|
502
|
+
/**
|
|
503
|
+
* The S3 bucket for media storage
|
|
504
|
+
*/
|
|
505
|
+
public readonly bucket: s3.Bucket;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The CloudFront distribution for media delivery
|
|
509
|
+
*/
|
|
510
|
+
public readonly distribution: cloudfront.Distribution;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The media CDN URL
|
|
514
|
+
*/
|
|
515
|
+
public readonly mediaUrl: string;
|
|
516
|
+
|
|
517
|
+
constructor(scope: Construct, id: string, props: MediaStackProps) {
|
|
518
|
+
super(scope, id, props);
|
|
519
|
+
|
|
520
|
+
const { stage } = props;
|
|
521
|
+
const isProd = stage === 'prod';
|
|
522
|
+
|
|
523
|
+
// Create S3 bucket for media storage
|
|
524
|
+
this.bucket = new s3.Bucket(this, 'MediaBucket', {
|
|
525
|
+
bucketName: \`${lowerName}-media-\${stage}-\${this.account}\`,
|
|
526
|
+
removalPolicy: isProd
|
|
527
|
+
? cdk.RemovalPolicy.RETAIN
|
|
528
|
+
: cdk.RemovalPolicy.DESTROY,
|
|
529
|
+
autoDeleteObjects: !isProd,
|
|
530
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
531
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
532
|
+
cors: [
|
|
533
|
+
{
|
|
534
|
+
allowedMethods: [
|
|
535
|
+
s3.HttpMethods.GET,
|
|
536
|
+
s3.HttpMethods.PUT,
|
|
537
|
+
s3.HttpMethods.POST,
|
|
538
|
+
],
|
|
539
|
+
allowedOrigins: ['*'],
|
|
540
|
+
allowedHeaders: ['*'],
|
|
541
|
+
maxAge: 3000,
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
lifecycleRules: [
|
|
545
|
+
{
|
|
546
|
+
id: 'DeleteIncompleteUploads',
|
|
547
|
+
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Create CloudFront distribution for media
|
|
553
|
+
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
|
554
|
+
comment: \`${options.projectName} Media CDN (\${stage})\`,
|
|
555
|
+
defaultBehavior: {
|
|
556
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
|
|
557
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
558
|
+
cachePolicy: new cloudfront.CachePolicy(this, 'MediaCachePolicy', {
|
|
559
|
+
cachePolicyName: \`${lowerName}-media-cache-\${stage}\`,
|
|
560
|
+
defaultTtl: cdk.Duration.days(30),
|
|
561
|
+
maxTtl: cdk.Duration.days(365),
|
|
562
|
+
minTtl: cdk.Duration.days(1),
|
|
563
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
|
|
564
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
|
|
565
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
566
|
+
enableAcceptEncodingGzip: true,
|
|
567
|
+
enableAcceptEncodingBrotli: true,
|
|
568
|
+
}),
|
|
569
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
570
|
+
compress: true,
|
|
571
|
+
},
|
|
572
|
+
priceClass: isProd
|
|
573
|
+
? cloudfront.PriceClass.PRICE_CLASS_ALL
|
|
574
|
+
: cloudfront.PriceClass.PRICE_CLASS_100,
|
|
575
|
+
enabled: true,
|
|
576
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Store media URL
|
|
580
|
+
this.mediaUrl = \`https://\${this.distribution.distributionDomainName}\`;
|
|
581
|
+
|
|
582
|
+
// Outputs
|
|
583
|
+
new cdk.CfnOutput(this, 'MediaUrl', {
|
|
584
|
+
value: this.mediaUrl,
|
|
585
|
+
description: 'Media CDN URL',
|
|
586
|
+
exportName: \`${pascalName}-MediaUrl-\${stage}\`,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
new cdk.CfnOutput(this, 'MediaBucketName', {
|
|
590
|
+
value: this.bucket.bucketName,
|
|
591
|
+
description: 'Media S3 bucket name',
|
|
592
|
+
exportName: \`${pascalName}-MediaBucket-\${stage}\`,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
new cdk.CfnOutput(this, 'MediaBucketArn', {
|
|
596
|
+
value: this.bucket.bucketArn,
|
|
597
|
+
description: 'Media S3 bucket ARN',
|
|
598
|
+
exportName: \`${pascalName}-MediaBucketArn-\${stage}\`,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
new cdk.CfnOutput(this, 'MediaDistributionId', {
|
|
602
|
+
value: this.distribution.distributionId,
|
|
603
|
+
description: 'Media CloudFront distribution ID',
|
|
604
|
+
exportName: \`${pascalName}-MediaDistributionId-\${stage}\`,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Returns the frontend-stack.ts template.
|
|
612
|
+
*
|
|
613
|
+
* @param projectName - Name of the project
|
|
614
|
+
* @returns Generated TypeScript code for frontend stack
|
|
615
|
+
*/
|
|
616
|
+
function getFrontendStackTemplate(projectName) {
|
|
617
|
+
const pascalName = toPascalCase(projectName);
|
|
618
|
+
const lowerName = projectName.toLowerCase();
|
|
619
|
+
return `/**
|
|
620
|
+
* Frontend Stack for ${projectName}
|
|
621
|
+
*
|
|
622
|
+
* Creates S3 bucket and CloudFront distribution for the frontend application.
|
|
623
|
+
*/
|
|
624
|
+
|
|
625
|
+
import * as cdk from 'aws-cdk-lib';
|
|
626
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
627
|
+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
|
628
|
+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
629
|
+
import { Construct } from 'constructs';
|
|
630
|
+
|
|
631
|
+
export interface FrontendStackProps extends cdk.StackProps {
|
|
632
|
+
stage: string;
|
|
633
|
+
apiUrl: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export class FrontendStack extends cdk.Stack {
|
|
637
|
+
/**
|
|
638
|
+
* The S3 bucket for frontend assets
|
|
639
|
+
*/
|
|
640
|
+
public readonly bucket: s3.Bucket;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* The CloudFront distribution for frontend delivery
|
|
644
|
+
*/
|
|
645
|
+
public readonly distribution: cloudfront.Distribution;
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The frontend URL
|
|
649
|
+
*/
|
|
650
|
+
public readonly url: string;
|
|
651
|
+
|
|
652
|
+
constructor(scope: Construct, id: string, props: FrontendStackProps) {
|
|
653
|
+
super(scope, id, props);
|
|
654
|
+
|
|
655
|
+
const { stage, apiUrl } = props;
|
|
656
|
+
const isProd = stage === 'prod';
|
|
657
|
+
|
|
658
|
+
// Create S3 bucket for frontend assets
|
|
659
|
+
this.bucket = new s3.Bucket(this, 'FrontendBucket', {
|
|
660
|
+
bucketName: \`${lowerName}-frontend-\${stage}-\${this.account}\`,
|
|
661
|
+
removalPolicy: isProd
|
|
662
|
+
? cdk.RemovalPolicy.RETAIN
|
|
663
|
+
: cdk.RemovalPolicy.DESTROY,
|
|
664
|
+
autoDeleteObjects: !isProd,
|
|
665
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
666
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Create CloudFront distribution for frontend
|
|
670
|
+
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
|
671
|
+
comment: \`${projectName} Frontend (\${stage})\`,
|
|
672
|
+
defaultBehavior: {
|
|
673
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
|
|
674
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
675
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
676
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
677
|
+
compress: true,
|
|
678
|
+
},
|
|
679
|
+
defaultRootObject: 'index.html',
|
|
680
|
+
// SPA routing - return index.html for 404 and 403 errors
|
|
681
|
+
errorResponses: [
|
|
682
|
+
{
|
|
683
|
+
httpStatus: 404,
|
|
684
|
+
responseHttpStatus: 200,
|
|
685
|
+
responsePagePath: '/index.html',
|
|
686
|
+
ttl: cdk.Duration.minutes(5),
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
httpStatus: 403,
|
|
690
|
+
responseHttpStatus: 200,
|
|
691
|
+
responsePagePath: '/index.html',
|
|
692
|
+
ttl: cdk.Duration.minutes(5),
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
priceClass: isProd
|
|
696
|
+
? cloudfront.PriceClass.PRICE_CLASS_ALL
|
|
697
|
+
: cloudfront.PriceClass.PRICE_CLASS_100,
|
|
698
|
+
enabled: true,
|
|
699
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Store frontend URL
|
|
703
|
+
this.url = \`https://\${this.distribution.distributionDomainName}\`;
|
|
704
|
+
|
|
705
|
+
// Outputs
|
|
706
|
+
new cdk.CfnOutput(this, 'FrontendUrl', {
|
|
707
|
+
value: this.url,
|
|
708
|
+
description: 'Frontend URL',
|
|
709
|
+
exportName: \`${pascalName}-FrontendUrl-\${stage}\`,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
new cdk.CfnOutput(this, 'FrontendBucketName', {
|
|
713
|
+
value: this.bucket.bucketName,
|
|
714
|
+
description: 'Frontend S3 bucket name',
|
|
715
|
+
exportName: \`${pascalName}-FrontendBucket-\${stage}\`,
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
new cdk.CfnOutput(this, 'FrontendDistributionId', {
|
|
719
|
+
value: this.distribution.distributionId,
|
|
720
|
+
description: 'Frontend CloudFront distribution ID',
|
|
721
|
+
exportName: \`${pascalName}-FrontendDistributionId-\${stage}\`,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
new cdk.CfnOutput(this, 'ApiUrl', {
|
|
725
|
+
value: apiUrl,
|
|
726
|
+
description: 'API URL (for reference)',
|
|
727
|
+
exportName: \`${pascalName}-Frontend-ApiUrl-\${stage}\`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Returns the cms-stack.ts template.
|
|
735
|
+
*
|
|
736
|
+
* @param projectName - Name of the project
|
|
737
|
+
* @returns Generated TypeScript code for CMS stack
|
|
738
|
+
*/
|
|
739
|
+
function getCmsStackTemplate(projectName) {
|
|
740
|
+
const pascalName = toPascalCase(projectName);
|
|
741
|
+
const lowerName = projectName.toLowerCase();
|
|
742
|
+
return `/**
|
|
743
|
+
* CMS Stack for ${projectName}
|
|
744
|
+
*
|
|
745
|
+
* Creates S3 bucket and CloudFront distribution for the CMS admin interface.
|
|
746
|
+
* Always uses PRICE_CLASS_100 as CMS is an internal admin tool.
|
|
747
|
+
*/
|
|
748
|
+
|
|
749
|
+
import * as cdk from 'aws-cdk-lib';
|
|
750
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
751
|
+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
|
752
|
+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
753
|
+
import { Construct } from 'constructs';
|
|
754
|
+
|
|
755
|
+
export interface CmsStackProps extends cdk.StackProps {
|
|
756
|
+
stage: string;
|
|
757
|
+
apiUrl: string;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export class CmsStack extends cdk.Stack {
|
|
761
|
+
/**
|
|
762
|
+
* The S3 bucket for CMS assets
|
|
763
|
+
*/
|
|
764
|
+
public readonly bucket: s3.Bucket;
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* The CloudFront distribution for CMS delivery
|
|
768
|
+
*/
|
|
769
|
+
public readonly distribution: cloudfront.Distribution;
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* The CMS URL
|
|
773
|
+
*/
|
|
774
|
+
public readonly url: string;
|
|
775
|
+
|
|
776
|
+
constructor(scope: Construct, id: string, props: CmsStackProps) {
|
|
777
|
+
super(scope, id, props);
|
|
778
|
+
|
|
779
|
+
const { stage, apiUrl } = props;
|
|
780
|
+
const isProd = stage === 'prod';
|
|
781
|
+
|
|
782
|
+
// Create S3 bucket for CMS assets
|
|
783
|
+
this.bucket = new s3.Bucket(this, 'CmsBucket', {
|
|
784
|
+
bucketName: \`${lowerName}-cms-\${stage}-\${this.account}\`,
|
|
785
|
+
removalPolicy: isProd
|
|
786
|
+
? cdk.RemovalPolicy.RETAIN
|
|
787
|
+
: cdk.RemovalPolicy.DESTROY,
|
|
788
|
+
autoDeleteObjects: !isProd,
|
|
789
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
790
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Create CloudFront distribution for CMS
|
|
794
|
+
// Always use PRICE_CLASS_100 - CMS is internal admin tool, not public-facing
|
|
795
|
+
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
|
796
|
+
comment: \`${projectName} CMS (\${stage})\`,
|
|
797
|
+
defaultBehavior: {
|
|
798
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
|
|
799
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
800
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
801
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
802
|
+
compress: true,
|
|
803
|
+
},
|
|
804
|
+
defaultRootObject: 'index.html',
|
|
805
|
+
// SPA routing - return index.html for 404 and 403 errors
|
|
806
|
+
errorResponses: [
|
|
807
|
+
{
|
|
808
|
+
httpStatus: 404,
|
|
809
|
+
responseHttpStatus: 200,
|
|
810
|
+
responsePagePath: '/index.html',
|
|
811
|
+
ttl: cdk.Duration.minutes(5),
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
httpStatus: 403,
|
|
815
|
+
responseHttpStatus: 200,
|
|
816
|
+
responsePagePath: '/index.html',
|
|
817
|
+
ttl: cdk.Duration.minutes(5),
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
// Always use PRICE_CLASS_100 for CMS (internal admin tool)
|
|
821
|
+
priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
|
|
822
|
+
enabled: true,
|
|
823
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Store CMS URL
|
|
827
|
+
this.url = \`https://\${this.distribution.distributionDomainName}\`;
|
|
828
|
+
|
|
829
|
+
// Outputs
|
|
830
|
+
new cdk.CfnOutput(this, 'CmsUrl', {
|
|
831
|
+
value: this.url,
|
|
832
|
+
description: 'CMS URL',
|
|
833
|
+
exportName: \`${pascalName}-CmsUrl-\${stage}\`,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
new cdk.CfnOutput(this, 'CmsBucketName', {
|
|
837
|
+
value: this.bucket.bucketName,
|
|
838
|
+
description: 'CMS S3 bucket name',
|
|
839
|
+
exportName: \`${pascalName}-CmsBucket-\${stage}\`,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
new cdk.CfnOutput(this, 'CmsDistributionId', {
|
|
843
|
+
value: this.distribution.distributionId,
|
|
844
|
+
description: 'CMS CloudFront distribution ID',
|
|
845
|
+
exportName: \`${pascalName}-CmsDistributionId-\${stage}\`,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
new cdk.CfnOutput(this, 'ApiUrl', {
|
|
849
|
+
value: apiUrl,
|
|
850
|
+
description: 'API URL (for reference)',
|
|
851
|
+
exportName: \`${pascalName}-Cms-ApiUrl-\${stage}\`,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Updates app.ts to add a new stack.
|
|
859
|
+
*
|
|
860
|
+
* This function performs incremental updates to the existing app.ts file:
|
|
861
|
+
* 1. Adds import statement for the new stack
|
|
862
|
+
* 2. Adds stack instantiation code
|
|
863
|
+
* 3. Adds dependency declarations
|
|
864
|
+
* 4. Updates the output summary if present
|
|
865
|
+
*
|
|
866
|
+
* @param appTsPath - Path to the app.ts file
|
|
867
|
+
* @param config - Stack configuration
|
|
868
|
+
* @param projectName - Name of the project (for console output)
|
|
869
|
+
* @returns true if update was successful, false if stack already exists
|
|
870
|
+
*/
|
|
871
|
+
function updateAppTsForStack(appTsPath, config, projectName) {
|
|
872
|
+
if (!(0, fs_1.existsSync)(appTsPath)) {
|
|
873
|
+
throw new Error(`app.ts not found at ${appTsPath}`);
|
|
874
|
+
}
|
|
875
|
+
const content = (0, fs_1.readFileSync)(appTsPath, 'utf-8');
|
|
876
|
+
// Check if stack already exists (prevent duplicates)
|
|
877
|
+
if (content.includes(`import { ${config.className} }`)) {
|
|
878
|
+
logger_js_1.logger.warn(`${config.className} is already imported in app.ts`);
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
const lines = content.split('\n');
|
|
882
|
+
const result = [];
|
|
883
|
+
let importInserted = false;
|
|
884
|
+
let stackInstantiated = false;
|
|
885
|
+
let summaryUpdated = false;
|
|
886
|
+
// Find the last stack import to insert after it
|
|
887
|
+
let lastStackImportIndex = -1;
|
|
888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
889
|
+
if (lines[i].match(/import \{ \w+Stack \} from '\.\/stacks\/[\w-]+';/)) {
|
|
890
|
+
lastStackImportIndex = i;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// Find the apiStack variable name (might be renamed by user)
|
|
894
|
+
const apiStackMatch = content.match(/const\s+(\w+)\s*=\s*new\s+ApiStack/);
|
|
895
|
+
const apiStackVarName = apiStackMatch ? apiStackMatch[1] : 'apiStack';
|
|
896
|
+
for (let i = 0; i < lines.length; i++) {
|
|
897
|
+
const line = lines[i];
|
|
898
|
+
// Insert import after last stack import
|
|
899
|
+
if (!importInserted && i === lastStackImportIndex) {
|
|
900
|
+
result.push(line);
|
|
901
|
+
result.push(`import { ${config.className} } from '${config.importPath}';`);
|
|
902
|
+
importInserted = true;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
// Insert stack instantiation after apiStack dependency declarations
|
|
906
|
+
// Look for the pattern: apiStack.addDependency(mediaStack);
|
|
907
|
+
if (!stackInstantiated &&
|
|
908
|
+
line.includes(`${apiStackVarName}.addDependency(mediaStack)`)) {
|
|
909
|
+
result.push(line);
|
|
910
|
+
result.push('');
|
|
911
|
+
result.push(`/**`);
|
|
912
|
+
result.push(` * ${config.name} Stack`);
|
|
913
|
+
result.push(` * ${config.description}`);
|
|
914
|
+
result.push(` */`);
|
|
915
|
+
result.push(`const ${config.variableName} = new ${config.className}(app, stackName('${config.name}'), ${config.props});`);
|
|
916
|
+
result.push('');
|
|
917
|
+
// Add dependencies
|
|
918
|
+
for (const dep of config.dependsOn) {
|
|
919
|
+
result.push(`${config.variableName}.addDependency(${dep});`);
|
|
920
|
+
}
|
|
921
|
+
stackInstantiated = true;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
// Update summary output if present
|
|
925
|
+
if (!summaryUpdated && line.includes('║ Stacks:')) {
|
|
926
|
+
result.push(line);
|
|
927
|
+
// Find the next lines with stack names and add our stack after ApiStack
|
|
928
|
+
let j = i + 1;
|
|
929
|
+
while (j < lines.length && lines[j].includes('║ -')) {
|
|
930
|
+
result.push(lines[j]);
|
|
931
|
+
if (lines[j].includes("stackName('Api')")) {
|
|
932
|
+
result.push(`║ - \${stackName('${config.name}')}`);
|
|
933
|
+
summaryUpdated = true;
|
|
934
|
+
}
|
|
935
|
+
j++;
|
|
936
|
+
}
|
|
937
|
+
// Skip the lines we've already processed
|
|
938
|
+
i = j - 1;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
result.push(line);
|
|
942
|
+
}
|
|
943
|
+
// Write the updated content
|
|
944
|
+
(0, fs_1.writeFileSync)(appTsPath, result.join('\n'));
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Validates that all required dependencies exist in app.ts content.
|
|
949
|
+
*
|
|
950
|
+
* @param content - app.ts file content
|
|
951
|
+
* @param requiredVars - Array of variable names that must exist
|
|
952
|
+
* @throws Error if any required variable is not found
|
|
953
|
+
*/
|
|
954
|
+
function validateDependencies(content, requiredVars) {
|
|
955
|
+
const missingDeps = [];
|
|
956
|
+
for (const varName of requiredVars) {
|
|
957
|
+
const pattern = new RegExp(`const\\s+${varName}\\s*=`);
|
|
958
|
+
if (!pattern.test(content)) {
|
|
959
|
+
missingDeps.push(varName);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (missingDeps.length > 0) {
|
|
963
|
+
throw new Error(`Missing required dependencies: ${missingDeps.join(', ')}. ` +
|
|
964
|
+
`Ensure these stacks are created before adding dependent stacks.`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Generates the frontend stack and updates app.ts.
|
|
969
|
+
*
|
|
970
|
+
* @param options - Generation options
|
|
971
|
+
*/
|
|
972
|
+
async function generateFrontendStack(options) {
|
|
973
|
+
const infraDir = (0, path_1.join)(options.projectPath, 'packages/infra');
|
|
974
|
+
const stacksDir = (0, path_1.join)(infraDir, 'src/stacks');
|
|
975
|
+
const appTsPath = (0, path_1.join)(infraDir, 'src/app.ts');
|
|
976
|
+
// Check if infra directory exists
|
|
977
|
+
if (!(0, fs_1.existsSync)(infraDir)) {
|
|
978
|
+
throw new Error('Infrastructure package not found at packages/infra. Run `fnd add api` first.');
|
|
979
|
+
}
|
|
980
|
+
// Validate apiStack exists in app.ts
|
|
981
|
+
const appTsContent = (0, fs_1.readFileSync)(appTsPath, 'utf-8');
|
|
982
|
+
validateDependencies(appTsContent, ['apiStack']);
|
|
983
|
+
// Ensure stacks directory exists
|
|
984
|
+
if (!(0, fs_1.existsSync)(stacksDir)) {
|
|
985
|
+
(0, fs_1.mkdirSync)(stacksDir, { recursive: true });
|
|
986
|
+
}
|
|
987
|
+
// Write frontend-stack.ts
|
|
988
|
+
const stackPath = (0, path_1.join)(stacksDir, 'frontend-stack.ts');
|
|
989
|
+
if ((0, fs_1.existsSync)(stackPath)) {
|
|
990
|
+
logger_js_1.logger.warn('frontend-stack.ts already exists, skipping creation');
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
(0, fs_1.writeFileSync)(stackPath, getFrontendStackTemplate(options.projectName));
|
|
994
|
+
logger_js_1.logger.success('Created frontend-stack.ts');
|
|
995
|
+
}
|
|
996
|
+
// Update app.ts
|
|
997
|
+
const stackConfig = {
|
|
998
|
+
name: 'Frontend',
|
|
999
|
+
className: 'FrontendStack',
|
|
1000
|
+
importPath: './stacks/frontend-stack',
|
|
1001
|
+
variableName: 'frontendStack',
|
|
1002
|
+
dependsOn: ['apiStack'],
|
|
1003
|
+
props: `{
|
|
1004
|
+
env,
|
|
1005
|
+
stage,
|
|
1006
|
+
apiUrl: apiStack.apiUrl,
|
|
1007
|
+
description: '${options.projectName}: Frontend',
|
|
1008
|
+
}`,
|
|
1009
|
+
description: 'Creates S3 bucket and CloudFront for frontend',
|
|
1010
|
+
};
|
|
1011
|
+
const updated = updateAppTsForStack(appTsPath, stackConfig, options.projectName);
|
|
1012
|
+
if (updated) {
|
|
1013
|
+
logger_js_1.logger.success('Updated app.ts with FrontendStack');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Generates the CMS stack and updates app.ts.
|
|
1018
|
+
*
|
|
1019
|
+
* @param options - Generation options
|
|
1020
|
+
*/
|
|
1021
|
+
async function generateCmsStack(options) {
|
|
1022
|
+
const infraDir = (0, path_1.join)(options.projectPath, 'packages/infra');
|
|
1023
|
+
const stacksDir = (0, path_1.join)(infraDir, 'src/stacks');
|
|
1024
|
+
const appTsPath = (0, path_1.join)(infraDir, 'src/app.ts');
|
|
1025
|
+
// Check if infra directory exists
|
|
1026
|
+
if (!(0, fs_1.existsSync)(infraDir)) {
|
|
1027
|
+
throw new Error('Infrastructure package not found at packages/infra. Run `fnd add api` first.');
|
|
1028
|
+
}
|
|
1029
|
+
// Validate apiStack exists in app.ts
|
|
1030
|
+
const appTsContent = (0, fs_1.readFileSync)(appTsPath, 'utf-8');
|
|
1031
|
+
validateDependencies(appTsContent, ['apiStack']);
|
|
1032
|
+
// Ensure stacks directory exists
|
|
1033
|
+
if (!(0, fs_1.existsSync)(stacksDir)) {
|
|
1034
|
+
(0, fs_1.mkdirSync)(stacksDir, { recursive: true });
|
|
1035
|
+
}
|
|
1036
|
+
// Write cms-stack.ts
|
|
1037
|
+
const stackPath = (0, path_1.join)(stacksDir, 'cms-stack.ts');
|
|
1038
|
+
if ((0, fs_1.existsSync)(stackPath)) {
|
|
1039
|
+
logger_js_1.logger.warn('cms-stack.ts already exists, skipping creation');
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
(0, fs_1.writeFileSync)(stackPath, getCmsStackTemplate(options.projectName));
|
|
1043
|
+
logger_js_1.logger.success('Created cms-stack.ts');
|
|
1044
|
+
}
|
|
1045
|
+
// Update app.ts
|
|
1046
|
+
const stackConfig = {
|
|
1047
|
+
name: 'Cms',
|
|
1048
|
+
className: 'CmsStack',
|
|
1049
|
+
importPath: './stacks/cms-stack',
|
|
1050
|
+
variableName: 'cmsStack',
|
|
1051
|
+
dependsOn: ['apiStack'],
|
|
1052
|
+
props: `{
|
|
1053
|
+
env,
|
|
1054
|
+
stage,
|
|
1055
|
+
apiUrl: apiStack.apiUrl,
|
|
1056
|
+
description: '${options.projectName}: CMS',
|
|
1057
|
+
}`,
|
|
1058
|
+
description: 'Creates S3 bucket and CloudFront for CMS admin',
|
|
1059
|
+
};
|
|
1060
|
+
const updated = updateAppTsForStack(appTsPath, stackConfig, options.projectName);
|
|
1061
|
+
if (updated) {
|
|
1062
|
+
logger_js_1.logger.success('Updated app.ts with CmsStack');
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
193
1065
|
//# sourceMappingURL=infra-generator.js.map
|