@fjall/generator 0.88.4 → 0.89.2

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/ast/astCdnParser.d.ts +15 -0
  3. package/dist/src/ast/astCdnParser.js +114 -0
  4. package/dist/src/ast/astCommonParser.d.ts +90 -0
  5. package/dist/src/ast/astCommonParser.js +351 -0
  6. package/dist/src/ast/astComputeParser.d.ts +14 -2
  7. package/dist/src/ast/astComputeParser.js +55 -9
  8. package/dist/src/ast/astDatabaseParser.d.ts +104 -0
  9. package/dist/src/ast/astDatabaseParser.js +275 -0
  10. package/dist/src/ast/astInfrastructureParser.d.ts +23 -277
  11. package/dist/src/ast/astInfrastructureParser.js +83 -1456
  12. package/dist/src/ast/astMessagingParser.d.ts +25 -0
  13. package/dist/src/ast/astMessagingParser.js +78 -0
  14. package/dist/src/ast/astNetworkParser.d.ts +70 -0
  15. package/dist/src/ast/astNetworkParser.js +219 -0
  16. package/dist/src/ast/astPatternParser.d.ts +80 -0
  17. package/dist/src/ast/astPatternParser.js +155 -0
  18. package/dist/src/ast/astStorageParser.d.ts +18 -0
  19. package/dist/src/ast/astStorageParser.js +164 -0
  20. package/dist/src/ast/index.d.ts +1 -0
  21. package/dist/src/ast/index.js +4 -0
  22. package/dist/src/dns/bindParser.d.ts +13 -0
  23. package/dist/src/dns/bindParser.js +224 -0
  24. package/dist/src/dns/bindWriter.d.ts +2 -0
  25. package/dist/src/dns/bindWriter.js +52 -0
  26. package/dist/src/dns/index.d.ts +4 -0
  27. package/dist/src/dns/index.js +4 -0
  28. package/dist/src/dns/infrastructureWriter.d.ts +2 -0
  29. package/dist/src/dns/infrastructureWriter.js +58 -0
  30. package/dist/src/dns/types.d.ts +82 -0
  31. package/dist/src/dns/types.js +52 -0
  32. package/dist/src/generation/common.d.ts +1 -16
  33. package/dist/src/generation/common.js +2 -28
  34. package/dist/src/generation/compute.js +77 -28
  35. package/dist/src/generation/index.d.ts +2 -1
  36. package/dist/src/generation/index.js +3 -1
  37. package/dist/src/generation/messagingConnections.d.ts +33 -0
  38. package/dist/src/generation/messagingConnections.js +73 -0
  39. package/dist/src/generation/storage.d.ts +5 -1
  40. package/dist/src/generation/storage.js +9 -1
  41. package/dist/src/generation/storageConnections.d.ts +3 -3
  42. package/dist/src/generation/storageConnections.js +8 -4
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +2 -0
  45. package/dist/src/planning/resourcePlanning.js +0 -2
  46. package/dist/src/presets/tierTypes.d.ts +4 -1
  47. package/dist/src/schemas/applicationSchemas.d.ts +854 -0
  48. package/dist/src/schemas/applicationSchemas.js +80 -0
  49. package/dist/src/schemas/baseSchemas.d.ts +206 -0
  50. package/dist/src/schemas/baseSchemas.js +248 -0
  51. package/dist/src/schemas/cdnSchemas.d.ts +61 -0
  52. package/dist/src/schemas/cdnSchemas.js +62 -0
  53. package/dist/src/schemas/computeSchemas.d.ts +723 -0
  54. package/dist/src/schemas/computeSchemas.js +727 -0
  55. package/dist/src/schemas/constants.d.ts +12 -8
  56. package/dist/src/schemas/constants.js +14 -4
  57. package/dist/src/schemas/databaseSchemas.d.ts +638 -0
  58. package/dist/src/schemas/databaseSchemas.js +366 -0
  59. package/dist/src/schemas/messagingSchemas.d.ts +20 -0
  60. package/dist/src/schemas/messagingSchemas.js +29 -0
  61. package/dist/src/schemas/networkSchemas.d.ts +246 -0
  62. package/dist/src/schemas/networkSchemas.js +125 -0
  63. package/dist/src/schemas/patternSchemas.d.ts +708 -0
  64. package/dist/src/schemas/patternSchemas.js +294 -0
  65. package/dist/src/schemas/resourceSchemas.d.ts +24 -3530
  66. package/dist/src/schemas/resourceSchemas.js +24 -2011
  67. package/dist/src/schemas/storageSchemas.d.ts +93 -0
  68. package/dist/src/schemas/storageSchemas.js +119 -0
  69. package/dist/src/util/errorUtils.d.ts +1 -2
  70. package/dist/src/util/errorUtils.js +1 -15
  71. package/dist/src/validation/patterns.d.ts +9 -0
  72. package/dist/src/validation/patterns.js +9 -0
  73. package/package.json +4 -3
@@ -1,215 +1,20 @@
1
1
  import * as ts from "typescript";
2
- import { ApplicationResourcePlanSchema, getZodErrorMessage, S3_STACK_PLACEMENTS, } from "../schemas/resourceSchemas.js";
2
+ import { ApplicationResourcePlanSchema, getZodErrorMessage, } from "../schemas/resourceSchemas.js";
3
3
  import { z } from "zod";
4
- import { constIncludes, S3_ENCRYPTION_TYPES, BACKUP_VAULT_TIERS, } from "../schemas/constants.js";
5
4
  import { COST_ALLOCATION_TAG } from "../generation/infrastructure.js";
6
- import { convertComputeResources } from "./astComputeParser.js";
7
- const S3_BUCKET_CLASSES = new Set(["S3Bucket"]);
8
- /** Base check for non-null, non-array objects */
9
- export function isPlainObject(value) {
10
- return typeof value === "object" && value !== null && !Array.isArray(value);
11
- }
12
- /** Type guard for identifier references */
13
- export function isIdentifierRef(value) {
14
- return isPlainObject(value) && "__identifier" in value;
15
- }
16
- /** Type guard for expression references */
17
- export function isExpressionRef(value) {
18
- return isPlainObject(value) && "__expression" in value;
19
- }
20
- /** Type guard for parsed objects */
21
- export function isParsedObject(value) {
22
- return (isPlainObject(value) &&
23
- !("__identifier" in value) &&
24
- !("__expression" in value) &&
25
- !("__call" in value) &&
26
- !("__unknown" in value));
27
- }
28
- /** Safely extract a string from ParsedValue */
29
- export function asString(value) {
30
- return typeof value === "string" ? value : undefined;
31
- }
32
- /** Safely extract a number from ParsedValue */
33
- export function asNumber(value) {
34
- return typeof value === "number" ? value : undefined;
35
- }
36
- /** Safely extract a boolean from ParsedValue */
37
- export function asBoolean(value) {
38
- return typeof value === "boolean" ? value : undefined;
39
- }
40
- /** Safely extract a string array from ParsedValue */
41
- export function asStringArray(value) {
42
- if (!Array.isArray(value))
43
- return undefined;
44
- const result = [];
45
- for (const item of value) {
46
- if (typeof item === "string")
47
- result.push(item);
48
- }
49
- return result.length > 0 ? result : undefined;
50
- }
51
- /** Check if an object literal property is a named property assignment */
52
- function isNamedProperty(prop, name) {
53
- return (ts.isPropertyAssignment(prop) &&
54
- ts.isIdentifier(prop.name) &&
55
- prop.name.text === name);
56
- }
57
- /** Check if a node is an App.getApp() call expression */
58
- function isAppGetAppCall(node) {
59
- return (ts.isCallExpression(node) &&
60
- ts.isPropertyAccessExpression(node.expression) &&
61
- node.expression.name.text === "getApp" &&
62
- ts.isIdentifier(node.expression.expression) &&
63
- node.expression.expression.text === "App");
64
- }
65
- /** Check if a node is a chained factory method call (e.g., app.addDatabase(...)) */
66
- function isFactoryMethodCall(node, methodName) {
67
- const { expression } = node;
68
- return (ts.isPropertyAccessExpression(expression) &&
69
- expression.name.text === methodName &&
70
- node.arguments.length > 0);
71
- }
72
- /** Traverse AST and return the first non-undefined result from the finder. */
73
- function findFirstInAst(sourceFile, finder) {
74
- let result;
75
- const visit = (node) => {
76
- if (result !== undefined)
77
- return;
78
- result = finder(node);
79
- if (result === undefined)
80
- ts.forEachChild(node, visit);
81
- };
82
- visit(sourceFile);
83
- return result;
84
- }
85
- /** Traverse AST and collect all non-null results from the collector. */
86
- function collectFromAst(sourceFile, collector) {
87
- const results = [];
88
- const visit = (node) => {
89
- const item = collector(node);
90
- if (item !== null)
91
- results.push(item);
92
- ts.forEachChild(node, visit);
93
- };
94
- visit(sourceFile);
95
- return results;
96
- }
97
- const FLOW_LOG_DESTINATIONS = ["cloudwatch", "s3"];
98
- function asFlowLogDestination(value) {
99
- if (value === undefined)
100
- return undefined;
101
- return FLOW_LOG_DESTINATIONS.includes(value)
102
- ? value
103
- : undefined;
104
- }
105
- function parseDeadLetterQueueConfig(value) {
106
- if (value === undefined || value === null)
107
- return undefined;
108
- if (value === false)
109
- return false;
110
- if (!isParsedObject(value))
111
- return undefined;
112
- return omitUndefined({
113
- enabled: asBoolean(value.enabled),
114
- maxReceiveCount: asNumber(value.maxReceiveCount),
115
- });
116
- }
117
- /** Safely extract an object or false from ParsedValue */
118
- function asObjectOrFalse(value) {
119
- if (value === undefined || value === null)
120
- return undefined;
121
- if (value === false)
122
- return false;
123
- if (typeof value === "object" && !Array.isArray(value))
124
- return value;
125
- return undefined;
126
- }
127
- function parseBooleanOrConfig(value, cast) {
128
- if (value === false)
129
- return false;
130
- return isParsedObject(value) ? cast(value) : undefined;
131
- }
132
- function parseOptionalConfig(value, parser) {
133
- return isParsedObject(value) ? parser(value) : undefined;
134
- }
135
- /** Extract a named sub-object from a config, returning undefined if not a plain object */
136
- function extractSubConfig(rawConfig, field, transform) {
137
- const value = rawConfig[field];
138
- return isParsedObject(value) ? transform(value) : undefined;
139
- }
140
- function extractVariableName(node) {
141
- const parent = node.parent;
142
- if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
143
- return parent.name.text;
144
- }
145
- return undefined;
146
- }
147
- /** Identity cast - trusts that parsed CDK output matches the expected shape.
148
- * Safe because input is always generated CDK code that was originally validated by Zod schemas. */
149
- const typed = () => (v) => v;
150
- /** Narrow a string to a union member, returning undefined if not a valid member */
151
- export function asStringUnion(value, validValues) {
152
- const str = asString(value);
153
- if (str === undefined)
154
- return undefined;
155
- return constIncludes(validValues, str) ? str : undefined;
156
- }
157
- /** Returns a shallow copy with all undefined-valued properties removed */
158
- function omitUndefined(obj) {
159
- const result = {};
160
- for (const [key, value] of Object.entries(obj)) {
161
- if (value !== undefined) {
162
- result[key] = value;
163
- }
164
- }
165
- return result;
166
- }
167
- /** Reconstruct source text from a parsed AST value for round-trip preservation */
168
- function parsedValueToSourceText(value) {
169
- if (value === null)
170
- return "null";
171
- if (value === undefined)
172
- return "undefined";
173
- if (typeof value === "string")
174
- return JSON.stringify(value);
175
- if (typeof value === "number" || typeof value === "boolean")
176
- return String(value);
177
- if (Array.isArray(value)) {
178
- return `[${value.map(parsedValueToSourceText).join(", ")}]`;
179
- }
180
- if (!isPlainObject(value))
181
- return String(value);
182
- if (isIdentifierRef(value))
183
- return value.__identifier;
184
- if (isExpressionRef(value))
185
- return value.__expression;
186
- if (isCallRef(value))
187
- return value.__call;
188
- if ("__unknown" in value && typeof value.__unknown === "string")
189
- return value.__unknown;
190
- const entries = Object.entries(value);
191
- if (entries.length === 0)
192
- return "{}";
193
- const inner = entries
194
- .map(([k, v]) => `${k}: ${parsedValueToSourceText(v)}`)
195
- .join(", ");
196
- return `{ ${inner} }`;
197
- }
198
- /** Type guard for call references */
199
- export function isCallRef(value) {
200
- return isPlainObject(value) && "__call" in value;
201
- }
202
- /** Capture properties from a parsed config that are NOT in the known keys set */
203
- export function captureExtraProperties(config, knownKeys) {
204
- const extras = [];
205
- for (const [key, value] of Object.entries(config)) {
206
- if (knownKeys.has(key) || value === undefined)
207
- continue;
208
- extras.push({ key, sourceText: parsedValueToSourceText(value) });
209
- }
210
- return extras;
211
- }
212
- const DYNAMODB_KEY_TYPES = ["S", "N", "B"];
5
+ // Re-export common types and utilities for downstream consumers
6
+ export { isPlainObject, isIdentifierRef, isExpressionRef, isCallRef, isParsedObject, asString, asNumber, asBoolean, asStringArray, asStringUnion, captureExtraProperties, } from "./astCommonParser.js";
7
+ export { findComputeResources, } from "./astComputeParser.js";
8
+ // Import sub-parsers
9
+ import { isAppGetAppCall, isExpressionRef, findFirstInAst, parseObjectLiteral, findVariableValue, evaluatePropertyAccess, } from "./astCommonParser.js";
10
+ import { findDatabaseResources, splitDynamoDBFromDatabases, convertDatabaseResources, convertDynamoDBResources, } from "./astDatabaseParser.js";
11
+ import { findS3Resources, findS3FactoryResources, convertS3Resources, S3_BUCKET_CLASSES, } from "./astStorageParser.js";
12
+ import { findComputeResources as findComputeResourcesFn, convertComputeResources, } from "./astComputeParser.js";
13
+ import { findSQSResources, convertSQSResources } from "./astMessagingParser.js";
14
+ import { findCDNResource, convertCDNResource } from "./astCdnParser.js";
15
+ import { findVpcId, findNetworkConfig, findBackupConfig, findTunnelConfig, findNetworkResources, convertNetworkConfig, convertBackupConfig, convertTunnelConfig, convertAdditionalNetworks, } from "./astNetworkParser.js";
16
+ import { findPatternResources, applyPatternConfig, } from "./astPatternParser.js";
17
+ // ---- Main entry point ----
213
18
  export function parseInfrastructure(content, options) {
214
19
  const sourceFile = ts.createSourceFile("infrastructure.ts", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
215
20
  const imports = extractImports(sourceFile);
@@ -227,7 +32,7 @@ export function parseInfrastructure(content, options) {
227
32
  ...findS3Resources(sourceFile),
228
33
  ...findS3FactoryResources(sourceFile),
229
34
  ];
230
- const computeResources = findComputeResources(sourceFile);
35
+ const computeResources = findComputeResourcesFn(sourceFile);
231
36
  const patternResources = findPatternResources(sourceFile);
232
37
  const tags = extractTags(sourceFile);
233
38
  const result = {
@@ -255,6 +60,7 @@ export function parseInfrastructure(content, options) {
255
60
  }
256
61
  return result;
257
62
  }
63
+ // ---- Import extraction ----
258
64
  function extractImports(sourceFile) {
259
65
  const imports = [];
260
66
  ts.forEachChild(sourceFile, (node) => {
@@ -281,6 +87,7 @@ function extractImports(sourceFile) {
281
87
  });
282
88
  return imports;
283
89
  }
90
+ // ---- App variable resolution ----
284
91
  /** Resolve the app name from the first argument of App.getApp(). */
285
92
  function resolveAppName(sourceFile, arg) {
286
93
  if (ts.isStringLiteral(arg))
@@ -310,739 +117,7 @@ function findAppVariable(sourceFile) {
310
117
  return undefined;
311
118
  }) ?? { appVariableName: "", appName: "" });
312
119
  }
313
- function findVpcId(sourceFile) {
314
- return findFirstInAst(sourceFile, (node) => {
315
- if (!isAppGetAppCall(node) || node.arguments.length <= 1)
316
- return undefined;
317
- const optionsArg = node.arguments[1];
318
- if (!ts.isObjectLiteralExpression(optionsArg))
319
- return undefined;
320
- for (const prop of optionsArg.properties) {
321
- if (isNamedProperty(prop, "network") &&
322
- ts.isObjectLiteralExpression(prop.initializer)) {
323
- for (const networkProp of prop.initializer.properties) {
324
- if (isNamedProperty(networkProp, "useExisting") &&
325
- ts.isStringLiteral(networkProp.initializer)) {
326
- return networkProp.initializer.text;
327
- }
328
- }
329
- }
330
- }
331
- return undefined;
332
- });
333
- }
334
- function parseNetworkConfigFields(parsed) {
335
- return {
336
- maxAzs: asNumber(parsed.maxAzs),
337
- natGateways: parseBooleanOrConfig(parsed.natGateways, (v) => ({
338
- count: asNumber(v.count),
339
- })),
340
- flowLogs: parseBooleanOrConfig(parsed.flowLogs, (v) => ({
341
- destination: asString(v.destination),
342
- retentionDays: asNumber(v.retentionDays),
343
- })),
344
- vpcEndpoints: isParsedObject(parsed.vpcEndpoints)
345
- ? parsed.vpcEndpoints
346
- : undefined,
347
- };
348
- }
349
- /**
350
- * Extract the network configuration from App.getApp() options.
351
- * This extracts the network config object (not useExisting, which is handled by findVpcId).
352
- */
353
- function findNetworkConfig(sourceFile) {
354
- return findFirstInAst(sourceFile, (node) => {
355
- if (!isAppGetAppCall(node) || node.arguments.length <= 1)
356
- return undefined;
357
- const optionsArg = node.arguments[1];
358
- if (!ts.isObjectLiteralExpression(optionsArg))
359
- return undefined;
360
- for (const prop of optionsArg.properties) {
361
- if (isNamedProperty(prop, "network")) {
362
- if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
363
- return undefined;
364
- if (ts.isObjectLiteralExpression(prop.initializer)) {
365
- const hasUseExisting = prop.initializer.properties.some((p) => isNamedProperty(p, "useExisting"));
366
- if (!hasUseExisting) {
367
- const parsed = parseObjectLiteral(prop.initializer);
368
- return parseNetworkConfigFields(parsed);
369
- }
370
- }
371
- }
372
- }
373
- return undefined;
374
- });
375
- }
376
- /**
377
- * Extract the backup configuration from App.getApp() options.
378
- * Returns { tier: string } when backup is configured, false when explicitly disabled, undefined when absent.
379
- */
380
- function findBackupConfig(sourceFile) {
381
- return findFirstInAst(sourceFile, (node) => {
382
- if (!isAppGetAppCall(node) || node.arguments.length <= 1)
383
- return undefined;
384
- const optionsArg = node.arguments[1];
385
- if (!ts.isObjectLiteralExpression(optionsArg))
386
- return undefined;
387
- for (const prop of optionsArg.properties) {
388
- if (isNamedProperty(prop, "backup")) {
389
- if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
390
- return false;
391
- if (ts.isObjectLiteralExpression(prop.initializer)) {
392
- const parsed = parseObjectLiteral(prop.initializer);
393
- const tier = asString(parsed.tier);
394
- if (tier)
395
- return { tier };
396
- }
397
- }
398
- }
399
- return undefined;
400
- });
401
- }
402
- /**
403
- * Extract the tunnel configuration from App.getApp() options.
404
- * Returns true when `tunnel: true`, { instanceType?: string } when object, false when explicitly disabled, undefined when absent.
405
- */
406
- function findTunnelConfig(sourceFile) {
407
- return findFirstInAst(sourceFile, (node) => {
408
- if (!isAppGetAppCall(node) || node.arguments.length <= 1)
409
- return undefined;
410
- const optionsArg = node.arguments[1];
411
- if (!ts.isObjectLiteralExpression(optionsArg))
412
- return undefined;
413
- for (const prop of optionsArg.properties) {
414
- if (isNamedProperty(prop, "tunnel")) {
415
- if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
416
- return false;
417
- if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword)
418
- return true;
419
- if (ts.isObjectLiteralExpression(prop.initializer)) {
420
- const parsed = parseObjectLiteral(prop.initializer);
421
- const instanceType = asString(parsed.instanceType);
422
- if (instanceType)
423
- return { instanceType };
424
- return {};
425
- }
426
- }
427
- }
428
- return undefined;
429
- });
430
- }
431
- /** Type guard for Factory.build() call expressions */
432
- function isFactoryBuildCall(node, factoryName) {
433
- return (ts.isCallExpression(node) &&
434
- ts.isPropertyAccessExpression(node.expression) &&
435
- node.expression.name.text === "build" &&
436
- ts.isIdentifier(node.expression.expression) &&
437
- node.expression.expression.text === factoryName);
438
- }
439
- /**
440
- * Find all app.addNetwork() calls and extract their configuration.
441
- */
442
- function findNetworkResources(sourceFile) {
443
- return collectFromAst(sourceFile, (node) => {
444
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addNetwork"))
445
- return null;
446
- const networkArg = node.arguments[0];
447
- if (!isFactoryBuildCall(networkArg, "NetworkFactory"))
448
- return null;
449
- return extractNetworkResource(node, networkArg);
450
- });
451
- }
452
- /**
453
- * Extract a network resource from a NetworkFactory.build() call.
454
- */
455
- function extractNetworkResource(addNetworkCall, buildCall) {
456
- if (buildCall.arguments.length < 2)
457
- return null;
458
- const nameArg = buildCall.arguments[0];
459
- const configArg = buildCall.arguments[1];
460
- if (!ts.isStringLiteral(nameArg))
461
- return null;
462
- const name = nameArg.text;
463
- const parsed = ts.isObjectLiteralExpression(configArg)
464
- ? parseObjectLiteral(configArg)
465
- : {};
466
- const networkFields = parseNetworkConfigFields(parsed);
467
- const resource = {
468
- name,
469
- config: omitUndefined({ ...networkFields }),
470
- node: addNetworkCall,
471
- };
472
- const varName = extractVariableName(addNetworkCall);
473
- if (varName)
474
- resource.variableName = varName;
475
- return resource;
476
- }
477
- function findVariableValue(sourceFile, varName) {
478
- return findFirstInAst(sourceFile, (node) => {
479
- if (!ts.isVariableStatement(node))
480
- return undefined;
481
- for (const declaration of node.declarationList.declarations) {
482
- if (ts.isIdentifier(declaration.name) &&
483
- declaration.name.text === varName &&
484
- declaration.initializer) {
485
- if (ts.isStringLiteral(declaration.initializer)) {
486
- return declaration.initializer.text;
487
- }
488
- if (ts.isObjectLiteralExpression(declaration.initializer)) {
489
- const obj = parseObjectLiteral(declaration.initializer);
490
- return JSON.stringify(obj);
491
- }
492
- }
493
- }
494
- return undefined;
495
- });
496
- }
497
- function findObjectDeclaration(sourceFile, objName) {
498
- return findFirstInAst(sourceFile, (node) => {
499
- if (!ts.isVariableStatement(node))
500
- return undefined;
501
- for (const declaration of node.declarationList.declarations) {
502
- if (ts.isIdentifier(declaration.name) &&
503
- declaration.name.text === objName &&
504
- declaration.initializer &&
505
- ts.isObjectLiteralExpression(declaration.initializer)) {
506
- return parseObjectLiteral(declaration.initializer);
507
- }
508
- }
509
- return undefined;
510
- });
511
- }
512
- function evaluatePropertyAccess(sourceFile, node) {
513
- if (!ts.isIdentifier(node.expression))
514
- return undefined;
515
- const objValue = findObjectDeclaration(sourceFile, node.expression.text);
516
- const propName = node.name.text;
517
- if (objValue && objValue[propName] !== undefined) {
518
- return String(objValue[propName]);
519
- }
520
- return undefined;
521
- }
522
- /** Derive database engine from parsed config */
523
- function extractDatabaseEngine(config) {
524
- const directEngine = asString(config.databaseEngine);
525
- if (directEngine === "postgresql" || directEngine === "mysql")
526
- return directEngine;
527
- // Derive from engine expression (e.g. DatabaseInstanceEngine.postgres(...))
528
- const engine = config.engine;
529
- if (isCallRef(engine)) {
530
- const callText = String(engine.__call);
531
- if (callText.includes("postgres") || callText.includes("auroraPostgres")) {
532
- return "postgresql";
533
- }
534
- if (callText.includes("mysql") || callText.includes("auroraMysql")) {
535
- return "mysql";
536
- }
537
- }
538
- return undefined;
539
- }
540
- /** Extract raw engine expression text (e.g. "DatabaseInstanceEngine.postgres(...)") */
541
- function extractEngineExpression(config) {
542
- const engine = config.engine;
543
- if (isCallRef(engine)) {
544
- return String(engine.__call);
545
- }
546
- return undefined;
547
- }
548
- /**
549
- * Resolve a template literal expression to its string value.
550
- * Walks spans: head.text + resolve each span.expression + span.literal.text.
551
- * Falls back to stripping backticks from getText() if resolution fails.
552
- */
553
- function resolveTemplateLiteral(sourceFile, templateExpr) {
554
- let result = templateExpr.head.text;
555
- for (const span of templateExpr.templateSpans) {
556
- if (ts.isIdentifier(span.expression)) {
557
- const resolved = findVariableValue(sourceFile, span.expression.text);
558
- if (resolved) {
559
- result += resolved;
560
- }
561
- else {
562
- // Can't resolve — fall back to raw getText()
563
- return templateExpr.getText(sourceFile).replace(/^`|`$/g, "");
564
- }
565
- }
566
- else if (ts.isPropertyAccessExpression(span.expression)) {
567
- const resolved = evaluatePropertyAccess(sourceFile, span.expression);
568
- if (resolved) {
569
- result += resolved;
570
- }
571
- else {
572
- return templateExpr.getText(sourceFile).replace(/^`|`$/g, "");
573
- }
574
- }
575
- else {
576
- return templateExpr.getText(sourceFile).replace(/^`|`$/g, "");
577
- }
578
- result += span.literal.text;
579
- }
580
- return result;
581
- }
582
- function findDatabaseResources(sourceFile) {
583
- return collectFromAst(sourceFile, (node) => {
584
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addDatabase"))
585
- return null;
586
- const databaseArg = node.arguments[0];
587
- if (!isFactoryBuildCall(databaseArg, "DatabaseFactory"))
588
- return null;
589
- return extractDatabaseResource(sourceFile, node, databaseArg);
590
- });
591
- }
592
- function extractDatabaseResource(sourceFile, addDatabaseCall, buildCall) {
593
- if (buildCall.arguments.length < 2)
594
- return null;
595
- const nameArg = buildCall.arguments[0];
596
- const configArg = buildCall.arguments[1];
597
- if (!ts.isStringLiteral(nameArg) && !ts.isTemplateExpression(nameArg)) {
598
- return null;
599
- }
600
- const resourceName = ts.isStringLiteral(nameArg)
601
- ? nameArg.text
602
- : resolveTemplateLiteral(sourceFile, nameArg);
603
- if (!ts.isObjectLiteralExpression(configArg)) {
604
- return null;
605
- }
606
- const config = parseObjectLiteral(configArg);
607
- const resource = {
608
- resourceName,
609
- type: asString(config.type) ?? "",
610
- databaseName: asString(config.databaseName) ?? "",
611
- databaseEngine: extractDatabaseEngine(config),
612
- engineExpression: extractEngineExpression(config),
613
- port: asNumber(config.port),
614
- deletionProtection: asBoolean(config.deletionProtection),
615
- instanceType: asString(config.instanceType),
616
- multiAz: asBoolean(config.multiAz),
617
- publiclyAccessible: asBoolean(config.publiclyAccessible),
618
- enableSecretRotation: asBoolean(config.enableSecretRotation),
619
- encryption: parseOptionalConfig(config.encryption, typed()),
620
- databaseInsights: parseBooleanOrConfig(config.databaseInsights, typed()),
621
- proxy: parseBooleanOrConfig(config.proxy, typed()),
622
- readReplica: parseBooleanOrConfig(config.readReplica, typed()),
623
- credentials: parseOptionalConfig(config.credentials, typed()),
624
- writer: parseOptionalConfig(config.writer, typed()),
625
- readers: parseBooleanOrConfig(config.readers, typed()),
626
- backupRetention: asNumber(config.backupRetention),
627
- preferredMaintenanceWindow: asString(config.preferredMaintenanceWindow),
628
- primaryRegion: asString(config.primaryRegion),
629
- secondaryRegions: asStringArray(config.secondaryRegions),
630
- globalClusterIdentifier: asString(config.globalClusterIdentifier),
631
- enableGlobalWriteForwarding: asBoolean(config.enableGlobalWriteForwarding),
632
- snapshotIdentifier: asString(config.snapshotIdentifier),
633
- snapshotUsername: asString(config.snapshotUsername),
634
- node: addDatabaseCall,
635
- };
636
- const varName = extractVariableName(addDatabaseCall);
637
- if (varName)
638
- resource.variableName = varName;
639
- const DATABASE_KNOWN_KEYS = new Set([
640
- "type",
641
- "databaseName",
642
- "databaseEngine",
643
- "engine",
644
- "vpc",
645
- "port",
646
- "deletionProtection",
647
- "instanceType",
648
- "multiAz",
649
- "publiclyAccessible",
650
- "enableSecretRotation",
651
- "encryption",
652
- "databaseInsights",
653
- "proxy",
654
- "readReplica",
655
- "credentials",
656
- "writer",
657
- "readers",
658
- "backupRetention",
659
- "preferredMaintenanceWindow",
660
- "primaryRegion",
661
- "secondaryRegions",
662
- "globalClusterIdentifier",
663
- "enableGlobalWriteForwarding",
664
- "snapshotIdentifier",
665
- "snapshotUsername",
666
- ]);
667
- const extras = captureExtraProperties(config, DATABASE_KNOWN_KEYS);
668
- if (extras.length > 0)
669
- resource.extraProperties = extras;
670
- return resource;
671
- }
672
- function findS3Resources(sourceFile) {
673
- return collectFromAst(sourceFile, (node) => {
674
- if (!ts.isVariableDeclaration(node) || !node.initializer)
675
- return null;
676
- if (!ts.isNewExpression(node.initializer))
677
- return null;
678
- const newExpr = node.initializer;
679
- if (!ts.isIdentifier(newExpr.expression))
680
- return null;
681
- const bucketClass = newExpr.expression.text;
682
- if (!S3_BUCKET_CLASSES.has(bucketClass))
683
- return null;
684
- return extractS3Resource(node, newExpr, bucketClass);
685
- });
686
- }
687
- function extractS3Resource(varDecl, newExpr, bucketClass) {
688
- const variableName = ts.isIdentifier(varDecl.name) ? varDecl.name.text : "";
689
- if (newExpr.arguments && newExpr.arguments.length >= 2) {
690
- const nameArg = newExpr.arguments[1];
691
- const resourceName = ts.isStringLiteral(nameArg) ? nameArg.text : "";
692
- const configArg = newExpr.arguments.length >= 3 ? newExpr.arguments[2] : undefined;
693
- const config = configArg && ts.isObjectLiteralExpression(configArg)
694
- ? parseObjectLiteral(configArg)
695
- : {};
696
- return {
697
- variableName,
698
- resourceName,
699
- bucketClass,
700
- config,
701
- node: varDecl,
702
- };
703
- }
704
- return null;
705
- }
706
- /** Find S3 resources created via app.addStorage(StorageFactory.build(...)) factory pattern */
707
- function findS3FactoryResources(sourceFile) {
708
- return collectFromAst(sourceFile, (node) => {
709
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addStorage"))
710
- return null;
711
- const storageArg = node.arguments[0];
712
- if (!isFactoryBuildCall(storageArg, "StorageFactory"))
713
- return null;
714
- return extractS3FactoryResource(node, storageArg);
715
- });
716
- }
717
- function extractS3FactoryResource(addStorageCall, buildCall) {
718
- if (buildCall.arguments.length < 2)
719
- return null;
720
- const nameArg = buildCall.arguments[0];
721
- const configArg = buildCall.arguments[1];
722
- if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(configArg))
723
- return null;
724
- const config = parseObjectLiteral(configArg);
725
- const variableName = extractVariableName(addStorageCall) ?? "";
726
- return {
727
- variableName,
728
- resourceName: nameArg.text,
729
- bucketClass: "StorageFactory",
730
- config,
731
- node: addStorageCall,
732
- };
733
- }
734
- function findComputeResources(sourceFile) {
735
- return collectFromAst(sourceFile, (node) => {
736
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addCompute"))
737
- return null;
738
- const computeArg = node.arguments[0];
739
- if (!isFactoryBuildCall(computeArg, "ComputeFactory"))
740
- return null;
741
- const resource = extractComputeResource(computeArg);
742
- if (resource) {
743
- const varName = extractVariableName(node);
744
- if (varName)
745
- resource.variableName = varName;
746
- }
747
- return resource;
748
- });
749
- }
750
- function findSQSResources(sourceFile) {
751
- return collectFromAst(sourceFile, (node) => {
752
- if (!ts.isCallExpression(node) ||
753
- !isFactoryMethodCall(node, "addMessaging"))
754
- return null;
755
- const messagingArg = node.arguments[0];
756
- if (!isFactoryBuildCall(messagingArg, "MessagingFactory"))
757
- return null;
758
- return extractSQSResource(node, messagingArg);
759
- });
760
- }
761
- function extractSQSResource(addMessagingCall, buildCall) {
762
- if (buildCall.arguments.length < 2)
763
- return null;
764
- const nameArg = buildCall.arguments[0];
765
- const configArg = buildCall.arguments[1];
766
- if (!ts.isStringLiteral(nameArg) ||
767
- !ts.isObjectLiteralExpression(configArg)) {
768
- return null;
769
- }
770
- const config = parseObjectLiteral(configArg);
771
- const resource = {
772
- resourceName: nameArg.text,
773
- queueType: asString(config.queueType) ?? "standard",
774
- visibilityTimeout: asNumber(config.visibilityTimeout),
775
- retentionPeriod: asNumber(config.messageRetentionPeriod),
776
- contentBasedDeduplication: asBoolean(config.contentBasedDeduplication),
777
- node: addMessagingCall,
778
- };
779
- const varName = extractVariableName(addMessagingCall);
780
- if (varName)
781
- resource.variableName = varName;
782
- const SQS_KNOWN_KEYS = new Set([
783
- "type",
784
- "queueType",
785
- "visibilityTimeout",
786
- "messageRetentionPeriod",
787
- "contentBasedDeduplication",
788
- ]);
789
- const extras = captureExtraProperties(config, SQS_KNOWN_KEYS);
790
- if (extras.length > 0)
791
- resource.extraProperties = extras;
792
- return resource;
793
- }
794
- function findCDNResource(sourceFile) {
795
- return findFirstInAst(sourceFile, (node) => {
796
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addCdn"))
797
- return undefined;
798
- const cdnArg = node.arguments[0];
799
- if (!isFactoryBuildCall(cdnArg, "CdnFactory"))
800
- return undefined;
801
- return extractCDNResource(cdnArg) ?? undefined;
802
- });
803
- }
804
- function extractCDNResource(buildCall) {
805
- if (buildCall.arguments.length < 2)
806
- return null;
807
- const nameArg = buildCall.arguments[0];
808
- const configArg = buildCall.arguments[1];
809
- if (!ts.isStringLiteral(nameArg) ||
810
- !ts.isObjectLiteralExpression(configArg)) {
811
- return null;
812
- }
813
- return {
814
- resourceName: nameArg.text,
815
- config: parseObjectLiteral(configArg),
816
- node: buildCall,
817
- };
818
- }
819
- function extractComputeResource(buildCall) {
820
- if (buildCall.arguments.length < 2)
821
- return null;
822
- const nameArg = buildCall.arguments[0];
823
- const configArg = buildCall.arguments[1];
824
- if (!ts.isStringLiteral(nameArg) ||
825
- !ts.isObjectLiteralExpression(configArg)) {
826
- return null;
827
- }
828
- const resourceName = nameArg.text;
829
- const config = parseObjectLiteral(configArg);
830
- return {
831
- resourceName,
832
- type: asString(config.type) ?? "ecs",
833
- config,
834
- node: buildCall,
835
- };
836
- }
837
- /**
838
- * Find pattern resources from app.addPattern(PatternFactory.build(...)) calls.
839
- * Patterns are high-level constructs that encapsulate multiple resources.
840
- */
841
- function findPatternResources(sourceFile) {
842
- return collectFromAst(sourceFile, (node) => {
843
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addPattern"))
844
- return null;
845
- const patternArg = node.arguments[0];
846
- if (!isFactoryBuildCall(patternArg, "PatternFactory"))
847
- return null;
848
- return extractPatternResource(patternArg, node);
849
- });
850
- }
851
- function parseLambdaConfig(obj) {
852
- if (!isParsedObject(obj))
853
- return undefined;
854
- const config = {
855
- ...(obj.memorySize !== undefined && {
856
- memorySize: asNumber(obj.memorySize),
857
- }),
858
- ...(obj.timeout !== undefined && { timeout: asNumber(obj.timeout) }),
859
- ...(obj.ephemeralStorageSize !== undefined && {
860
- ephemeralStorageSize: asNumber(obj.ephemeralStorageSize),
861
- }),
862
- };
863
- return Object.keys(config).length > 0 ? config : undefined;
864
- }
865
- const VALID_DB_TYPES = ["Instance", "Aurora", "GlobalAurora"];
866
- const VALID_NON_GLOBAL_DB_TYPES = ["Instance", "Aurora"];
867
- const VALID_DB_ENGINES = ["postgresql", "mysql"];
868
- function extractPatternDatabaseConfig(rawConfig) {
869
- return extractSubConfig(rawConfig, "database", (db) => ({
870
- type: asStringUnion(db.type, VALID_NON_GLOBAL_DB_TYPES),
871
- databaseName: asString(db.databaseName),
872
- databaseEngine: asStringUnion(db.databaseEngine, VALID_DB_ENGINES),
873
- deletionProtection: asBoolean(db.deletionProtection),
874
- backupRetention: asNumber(db.backupRetention),
875
- port: asNumber(db.port),
876
- publiclyAccessible: asBoolean(db.publiclyAccessible),
877
- allowedIpCidr: asString(db.allowedIpCidr),
878
- instanceType: asString(db.instanceType),
879
- allocatedStorage: asNumber(db.allocatedStorage),
880
- multiAz: asBoolean(db.multiAz),
881
- allowVpcAccess: asBoolean(db.allowVpcAccess),
882
- monitoringInterval: asNumber(db.monitoringInterval),
883
- preferredMaintenanceWindow: asString(db.preferredMaintenanceWindow),
884
- snapshotIdentifier: asString(db.snapshotIdentifier),
885
- snapshotUsername: asString(db.snapshotUsername),
886
- readReplica: asObjectOrFalse(db.readReplica),
887
- writer: parseOptionalConfig(db.writer, typed()),
888
- readers: asObjectOrFalse(db.readers),
889
- databaseInsights: asObjectOrFalse(db.databaseInsights),
890
- proxy: asObjectOrFalse(db.proxy),
891
- credentials: parseOptionalConfig(db.credentials, typed()),
892
- encryption: parseOptionalConfig(db.encryption, typed()),
893
- }));
894
- }
895
- function extractPatternComputeConfig(rawConfig) {
896
- return extractSubConfig(rawConfig, "compute", (compute) => ({
897
- server: parseOptionalConfig(compute.server, parseLambdaConfig),
898
- imageOptimisation: parseOptionalConfig(compute.imageOptimisation, parseLambdaConfig),
899
- revalidation: parseOptionalConfig(compute.revalidation, parseLambdaConfig),
900
- }));
901
- }
902
- function extractPatternStorageConfig(rawConfig) {
903
- const parseVersionedConfig = (obj) => ({
904
- versioned: asBoolean(obj.versioned),
905
- });
906
- return extractSubConfig(rawConfig, "storage", (storage) => ({
907
- assets: parseOptionalConfig(storage.assets, parseVersionedConfig),
908
- cache: parseOptionalConfig(storage.cache, parseVersionedConfig),
909
- media: parseOptionalConfig(storage.media, parseVersionedConfig),
910
- }));
911
- }
912
- function extractPatternMessagingConfig(rawConfig) {
913
- return extractSubConfig(rawConfig, "messaging", (messaging) => ({
914
- revalidationQueue: parseOptionalConfig(messaging.revalidationQueue, (queue) => ({
915
- visibilityTimeout: asNumber(queue.visibilityTimeout),
916
- messageRetentionPeriod: asNumber(queue.messageRetentionPeriod),
917
- maxMessageSize: asNumber(queue.maxMessageSize),
918
- deadLetterQueue: parseDeadLetterQueueConfig(queue.deadLetterQueue),
919
- })),
920
- }));
921
- }
922
- function extractPatternCdnConfig(rawConfig) {
923
- return extractSubConfig(rawConfig, "cdn", (cdn) => ({
924
- domainNames: asStringArray(cdn.domainNames),
925
- certificateArn: asString(cdn.certificateArn),
926
- }));
927
- }
928
- function extractPatternEnvironmentConfig(rawConfig) {
929
- return extractSubConfig(rawConfig, "environment", (env) => {
930
- const environment = Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
931
- return Object.keys(environment).length > 0 ? environment : undefined;
932
- });
933
- }
934
- /**
935
- * Extract pattern resource details from PatternFactory.build() call.
936
- */
937
- function extractPatternResource(buildCall, addPatternCall) {
938
- if (buildCall.arguments.length < 2)
939
- return null;
940
- const constructIdArg = buildCall.arguments[0];
941
- const configArg = buildCall.arguments[1];
942
- if (!ts.isStringLiteral(constructIdArg) ||
943
- !ts.isObjectLiteralExpression(configArg)) {
944
- return null;
945
- }
946
- const constructId = constructIdArg.text;
947
- const rawConfig = parseObjectLiteral(configArg);
948
- const patternType = rawConfig.type;
949
- if (patternType !== "payload" && patternType !== "nextjs") {
950
- return null;
951
- }
952
- const variableName = extractVariableName(addPatternCall);
953
- const name = asString(rawConfig.name);
954
- if (!name) {
955
- return null;
956
- }
957
- return {
958
- variableName,
959
- constructId,
960
- type: patternType,
961
- config: {
962
- name,
963
- domain: asString(rawConfig.domain),
964
- database: extractPatternDatabaseConfig(rawConfig),
965
- compute: extractPatternComputeConfig(rawConfig),
966
- storage: extractPatternStorageConfig(rawConfig),
967
- messaging: extractPatternMessagingConfig(rawConfig),
968
- cdn: extractPatternCdnConfig(rawConfig),
969
- environment: extractPatternEnvironmentConfig(rawConfig),
970
- },
971
- node: buildCall,
972
- };
973
- }
974
- function parseObjectLiteral(node) {
975
- const result = {};
976
- for (const prop of node.properties) {
977
- if (!ts.isPropertyAssignment(prop))
978
- continue;
979
- const key = ts.isIdentifier(prop.name)
980
- ? prop.name.text
981
- : ts.isStringLiteral(prop.name)
982
- ? prop.name.text
983
- : undefined;
984
- if (key === undefined)
985
- continue;
986
- result[key] = evaluateExpression(prop.initializer);
987
- }
988
- return result;
989
- }
990
- function evaluateExpression(node) {
991
- if (ts.isStringLiteral(node)) {
992
- return node.text;
993
- }
994
- else if (ts.isNumericLiteral(node)) {
995
- return Number(node.text);
996
- }
997
- else if (node.kind === ts.SyntaxKind.TrueKeyword) {
998
- return true;
999
- }
1000
- else if (node.kind === ts.SyntaxKind.FalseKeyword) {
1001
- return false;
1002
- }
1003
- else if (ts.isArrayLiteralExpression(node)) {
1004
- return node.elements.map((element) => evaluateExpression(element));
1005
- }
1006
- else if (ts.isObjectLiteralExpression(node)) {
1007
- return parseObjectLiteral(node);
1008
- }
1009
- else if (ts.isIdentifier(node)) {
1010
- return { __identifier: node.text };
1011
- }
1012
- else if (ts.isPropertyAccessExpression(node)) {
1013
- return { __expression: node.getText() };
1014
- }
1015
- else if (ts.isCallExpression(node)) {
1016
- return { __call: node.getText() };
1017
- }
1018
- else if (ts.isBinaryExpression(node) &&
1019
- node.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
1020
- // Handle || operator - return the left side if it's a literal
1021
- const left = evaluateExpression(node.left);
1022
- const right = evaluateExpression(node.right);
1023
- // For simplicity, prefer string literals from the right side for defaults
1024
- if (typeof right === "string") {
1025
- return right;
1026
- }
1027
- return left;
1028
- }
1029
- else if (ts.isConditionalExpression(node)) {
1030
- // Handle ternary operator - for simplicity, try to evaluate the branches
1031
- const whenTrue = evaluateExpression(node.whenTrue);
1032
- const whenFalse = evaluateExpression(node.whenFalse);
1033
- // Default to the false branch for simplicity
1034
- if (typeof whenFalse === "string" || typeof whenFalse === "number") {
1035
- return whenFalse;
1036
- }
1037
- return whenTrue;
1038
- }
1039
- else if (ts.isTemplateExpression(node) ||
1040
- ts.isNoSubstitutionTemplateLiteral(node)) {
1041
- // Handle template literals to preserve them as expressions during round-trip parsing
1042
- return { __expression: node.getText() };
1043
- }
1044
- return { __unknown: node.getText() };
1045
- }
120
+ // ---- Tags extraction ----
1046
121
  function extractTags(sourceFile) {
1047
122
  const tags = {};
1048
123
  function visit(node) {
@@ -1070,427 +145,68 @@ function extractTags(sourceFile) {
1070
145
  visit(sourceFile);
1071
146
  return tags;
1072
147
  }
1073
- /** Builds the conditional spread fields shared by primary and additional network configs */
1074
- function buildNetworkFields(config) {
1075
- const flowLogs = config.flowLogs;
1076
- return {
1077
- ...omitUndefined({
1078
- maxAzs: config.maxAzs,
1079
- natGateways: config.natGateways,
1080
- vpcEndpoints: config.vpcEndpoints,
1081
- }),
1082
- ...(flowLogs !== undefined && {
1083
- flowLogs: flowLogs === false
1084
- ? false
1085
- : omitUndefined({
1086
- destination: asFlowLogDestination(flowLogs.destination),
1087
- retentionDays: flowLogs.retentionDays,
1088
- }),
1089
- }),
1090
- };
1091
- }
1092
- function convertNetworkConfig(parsed) {
1093
- if (!parsed.network)
1094
- return undefined;
1095
- return buildNetworkFields(parsed.network);
1096
- }
1097
- function convertBackupConfig(parsed) {
1098
- if (parsed.backup === undefined)
1099
- return undefined;
1100
- if (parsed.backup === false)
1101
- return false;
1102
- const tier = parsed.backup.tier;
1103
- if (constIncludes(BACKUP_VAULT_TIERS, tier)) {
1104
- return { tier: tier };
148
+ // ---- Plan conversion ----
149
+ function applyTagsAndOwner(plan, tags) {
150
+ if (Object.keys(tags).length > 0) {
151
+ plan.tags = { ...tags };
1105
152
  }
1106
- return undefined;
1107
- }
1108
- function convertTunnelConfig(parsed) {
1109
- if (parsed.tunnel === undefined)
1110
- return undefined;
1111
- if (parsed.tunnel === false)
1112
- return false;
1113
- if (parsed.tunnel === true)
1114
- return {};
1115
- const result = {};
1116
- if (parsed.tunnel.instanceType) {
1117
- result.instanceType = parsed.tunnel.instanceType;
153
+ if (tags[COST_ALLOCATION_TAG]) {
154
+ plan.owner = tags[COST_ALLOCATION_TAG];
1118
155
  }
1119
- return result;
1120
156
  }
1121
- /** Parse a DynamoDB key schema from parsed AST object */
1122
- function parseDynamoDBKey(value) {
1123
- if (!isParsedObject(value))
1124
- return undefined;
1125
- const name = asString(value.name);
1126
- const type = asStringUnion(value.type, DYNAMODB_KEY_TYPES);
1127
- if (!name || !type)
1128
- return undefined;
1129
- return { name, type };
157
+ /** Imports that the generator always handles -- don't preserve as "additional" */
158
+ const KNOWN_FJALL_IMPORTS = new Set([
159
+ "App",
160
+ "Architecture",
161
+ "DatabaseFactory",
162
+ "StorageFactory",
163
+ "ComputeFactory",
164
+ "getConfig",
165
+ "MessagingFactory",
166
+ "CdnFactory",
167
+ "Code",
168
+ "Runtime",
169
+ "FunctionUrlAuthType",
170
+ "NetworkFactory",
171
+ "PatternFactory",
172
+ ]);
173
+ /** Check if a module specifier belongs to a managed (Fjall/CDK) package */
174
+ function isManagedModuleSpecifier(specifier) {
175
+ if (specifier.startsWith("@fjall/"))
176
+ return true;
177
+ if (specifier === "aws-cdk-lib" || specifier.startsWith("aws-cdk-lib/"))
178
+ return true;
179
+ if (specifier === "constructs")
180
+ return true;
181
+ return false;
1130
182
  }
1131
- /** Split DynamoDB resources (type: "DynamoDB") from regular database resources */
1132
- function splitDynamoDBFromDatabases(allResources) {
1133
- const databases = [];
1134
- const dynamodb = [];
1135
- for (const resource of allResources) {
1136
- if (resource.type === "DynamoDB") {
1137
- const dynamo = extractDynamoDBFields(resource);
1138
- if (dynamo)
1139
- dynamodb.push(dynamo);
183
+ /**
184
+ * Compute additional managed imports that the generator doesn't handle.
185
+ */
186
+ function computeAdditionalManagedImports(imports) {
187
+ const additional = [];
188
+ for (const imp of imports) {
189
+ if (!isManagedModuleSpecifier(imp.moduleSpecifier))
190
+ continue;
191
+ if (imp.moduleSpecifier === "@fjall/components-infrastructure" ||
192
+ imp.moduleSpecifier === "@fjall/infrastructure") {
193
+ const extraNames = imp.namedImports.filter((n) => !KNOWN_FJALL_IMPORTS.has(n));
194
+ if (extraNames.length > 0) {
195
+ additional.push({
196
+ moduleSpecifier: imp.moduleSpecifier,
197
+ namedImports: extraNames,
198
+ defaultImport: imp.defaultImport,
199
+ });
200
+ }
1140
201
  }
1141
202
  else {
1142
- databases.push(resource);
1143
- }
1144
- }
1145
- return { databases, dynamodb };
1146
- }
1147
- /** Extract DynamoDB-specific fields from a parsed database resource */
1148
- function extractDynamoDBFields(resource) {
1149
- // Re-read the raw config from the AST node to get DynamoDB-specific fields
1150
- // The resource was extracted by extractDatabaseResource which parsed the full config
1151
- const node = resource.node;
1152
- if (!ts.isCallExpression(node))
1153
- return null;
1154
- const buildCallArg = node.arguments[0];
1155
- if (!ts.isCallExpression(buildCallArg) || buildCallArg.arguments.length < 2)
1156
- return null;
1157
- const configArg = buildCallArg.arguments[1];
1158
- if (!ts.isObjectLiteralExpression(configArg))
1159
- return null;
1160
- const config = parseObjectLiteral(configArg);
1161
- const partitionKey = parseDynamoDBKey(config.partitionKey);
1162
- if (!partitionKey)
1163
- return null;
1164
- const result = {
1165
- resourceName: resource.resourceName,
1166
- partitionKey,
1167
- node: resource.node,
1168
- };
1169
- if (resource.variableName)
1170
- result.variableName = resource.variableName;
1171
- const sortKey = parseDynamoDBKey(config.sortKey);
1172
- if (sortKey)
1173
- result.sortKey = sortKey;
1174
- if (Array.isArray(config.globalSecondaryIndexes)) {
1175
- const gsis = config.globalSecondaryIndexes
1176
- .filter(isParsedObject)
1177
- .map((gsi) => {
1178
- const pk = parseDynamoDBKey(gsi.partitionKey);
1179
- if (!pk)
1180
- return null;
1181
- const indexName = asString(gsi.indexName);
1182
- if (!indexName)
1183
- return null;
1184
- const entry = { indexName, partitionKey: pk };
1185
- const sk = parseDynamoDBKey(gsi.sortKey);
1186
- if (sk)
1187
- entry.sortKey = sk;
1188
- return entry;
1189
- })
1190
- .filter((g) => g !== null);
1191
- if (gsis.length > 0)
1192
- result.globalSecondaryIndexes = gsis;
1193
- }
1194
- const ttlAttribute = asString(config.ttlAttribute);
1195
- if (ttlAttribute)
1196
- result.ttlAttribute = ttlAttribute;
1197
- const stream = asBoolean(config.stream);
1198
- if (stream !== undefined)
1199
- result.stream = stream;
1200
- const DYNAMODB_KNOWN_KEYS = new Set([
1201
- "type",
1202
- "databaseName",
1203
- "partitionKey",
1204
- "sortKey",
1205
- "globalSecondaryIndexes",
1206
- "ttlAttribute",
1207
- "stream",
1208
- ]);
1209
- const extras = captureExtraProperties(config, DYNAMODB_KNOWN_KEYS);
1210
- if (extras.length > 0)
1211
- result.extraProperties = extras;
1212
- return result;
1213
- }
1214
- /** Convert parsed DynamoDB resources to plan format */
1215
- function convertDynamoDBResources(parsed) {
1216
- if (parsed.dynamodbResources.length === 0)
1217
- return undefined;
1218
- return parsed.dynamodbResources.map((table) => ({
1219
- name: table.resourceName,
1220
- partitionKey: table.partitionKey,
1221
- ...omitUndefined({
1222
- sortKey: table.sortKey,
1223
- globalSecondaryIndexes: table.globalSecondaryIndexes,
1224
- ttlAttribute: table.ttlAttribute,
1225
- stream: table.stream,
1226
- variableName: table.variableName,
1227
- extraProperties: table.extraProperties,
1228
- }),
1229
- }));
1230
- }
1231
- /** Convert parsed SQS resources to plan format */
1232
- function convertSQSResources(parsed) {
1233
- if (parsed.sqsResources.length === 0)
1234
- return undefined;
1235
- return parsed.sqsResources.map((queue) => ({
1236
- name: queue.resourceName,
1237
- queueType: queue.queueType === "fifo" ? "fifo" : "standard",
1238
- ...omitUndefined({
1239
- visibilityTimeout: queue.visibilityTimeout,
1240
- retentionPeriod: queue.retentionPeriod,
1241
- contentBasedDeduplication: queue.contentBasedDeduplication,
1242
- variableName: queue.variableName,
1243
- extraProperties: queue.extraProperties,
1244
- }),
1245
- }));
1246
- }
1247
- /** Resolve a CDN origin identifier back to its resource name */
1248
- function resolveOriginIdentifier(identifier, s3Resources, computeResources) {
1249
- const s3Match = s3Resources.find((s3) => s3.variableName === identifier);
1250
- if (s3Match)
1251
- return s3Match.resourceName;
1252
- // Compute variable names are camelCase of resource name (e.g., myApi → MyApi)
1253
- const computeMatch = computeResources.find((c) => c.variableName === identifier ||
1254
- c.resourceName.charAt(0).toLowerCase() + c.resourceName.slice(1) ===
1255
- identifier);
1256
- if (computeMatch)
1257
- return computeMatch.resourceName;
1258
- return identifier;
1259
- }
1260
- const CDN_CACHE_POLICIES = ["CACHING_OPTIMIZED", "CACHING_DISABLED"];
1261
- /** Convert parsed CDN resource to plan format */
1262
- function convertCDNResource(parsed) {
1263
- if (!parsed.cdnResource)
1264
- return undefined;
1265
- const cdn = parsed.cdnResource;
1266
- const config = cdn.config;
1267
- const resolveOrigin = (val) => {
1268
- if (isIdentifierRef(val)) {
1269
- return resolveOriginIdentifier(val.__identifier, parsed.s3Resources, parsed.computeResources);
1270
- }
1271
- return asString(val) ?? "";
1272
- };
1273
- const defaultOriginRef = resolveOrigin(config.origin);
1274
- const result = {
1275
- name: cdn.resourceName,
1276
- defaultOriginRef,
1277
- };
1278
- if (Array.isArray(config.behaviours)) {
1279
- const behaviours = config.behaviours.filter(isParsedObject).map((b) => ({
1280
- pathPattern: asString(b.pathPattern) ?? "",
1281
- originRef: resolveOrigin(b.origin),
1282
- ...omitUndefined({
1283
- cachePolicy: asStringUnion(b.cachePolicy, CDN_CACHE_POLICIES),
1284
- }),
1285
- }));
1286
- if (behaviours.length > 0)
1287
- result.behaviours = behaviours;
1288
- }
1289
- // customDomain: direct string property or first element of domainNames array
1290
- const customDomain = asString(config.customDomain);
1291
- if (customDomain) {
1292
- result.customDomain = customDomain;
1293
- }
1294
- else {
1295
- const domainNames = asStringArray(config.domainNames);
1296
- if (domainNames && domainNames.length > 0) {
1297
- result.customDomain = domainNames[0];
1298
- }
1299
- }
1300
- const certificateArn = asString(config.certificateArn);
1301
- if (certificateArn)
1302
- result.certificateArn = certificateArn;
1303
- // Extract accessGate — can be false or { type, username, password }
1304
- if (config.accessGate !== undefined) {
1305
- if (config.accessGate === false) {
1306
- result.accessGate = false;
1307
- }
1308
- else if (isParsedObject(config.accessGate)) {
1309
- const gate = config.accessGate;
1310
- const gateType = asString(gate.type);
1311
- const username = asString(gate.username);
1312
- const password = asString(gate.password);
1313
- if (gateType === "basic-auth" && username && password) {
1314
- result.accessGate = { type: "basic-auth", username, password };
203
+ // aws-cdk-lib/*, constructs -- preserve entire import
204
+ if (imp.namedImports.length > 0 || imp.defaultImport) {
205
+ additional.push(imp);
1315
206
  }
1316
207
  }
1317
208
  }
1318
- const CDN_KNOWN_KEYS = new Set([
1319
- "originType",
1320
- "origin",
1321
- "behaviours",
1322
- "customDomain",
1323
- "domainNames",
1324
- "certificateArn",
1325
- "accessGate",
1326
- ]);
1327
- const extras = captureExtraProperties(config, CDN_KNOWN_KEYS);
1328
- if (extras.length > 0)
1329
- result.extraProperties = extras;
1330
- return result;
1331
- }
1332
- function convertDatabaseResources(parsed) {
1333
- return parsed.databaseResources
1334
- .filter((db) => db.type !== "DynamoDB")
1335
- .map((database) => ({
1336
- name: database.resourceName,
1337
- type: constIncludes(VALID_DB_TYPES, database.type)
1338
- ? database.type
1339
- : "Instance",
1340
- databaseName: database.databaseName,
1341
- ...omitUndefined({
1342
- port: database.port,
1343
- deletionProtection: database.deletionProtection,
1344
- instanceType: database.instanceType,
1345
- multiAz: database.multiAz,
1346
- publiclyAccessible: database.publiclyAccessible,
1347
- enableSecretRotation: database.enableSecretRotation,
1348
- encryption: database.encryption,
1349
- databaseInsights: database.databaseInsights,
1350
- proxy: database.proxy,
1351
- readReplica: database.readReplica,
1352
- credentials: database.credentials,
1353
- writer: database.writer,
1354
- readers: database.readers,
1355
- backupRetention: database.backupRetention,
1356
- preferredMaintenanceWindow: database.preferredMaintenanceWindow,
1357
- primaryRegion: database.primaryRegion,
1358
- secondaryRegions: database.secondaryRegions,
1359
- globalClusterIdentifier: database.globalClusterIdentifier,
1360
- enableGlobalWriteForwarding: database.enableGlobalWriteForwarding,
1361
- variableName: database.variableName,
1362
- databaseEngine: database.databaseEngine,
1363
- engineExpression: database.engineExpression,
1364
- extraProperties: database.extraProperties,
1365
- }),
1366
- }));
1367
- }
1368
- function convertS3Resources(parsed) {
1369
- return parsed.s3Resources.map((s3) => ({
1370
- name: s3.resourceName,
1371
- ...(asString(s3.config.bucketName) && {
1372
- bucketName: asString(s3.config.bucketName),
1373
- }),
1374
- ...(asBoolean(s3.config.publicReadAccess) === true && {
1375
- publicReadAccess: true,
1376
- }),
1377
- ...(isParsedObject(s3.config.websiteHosting) &&
1378
- (() => {
1379
- const hosting = s3.config.websiteHosting;
1380
- return {
1381
- websiteHosting: {
1382
- indexDocument: asString(hosting.indexDocument) ?? "index.html",
1383
- ...(asString(hosting.errorDocument) && {
1384
- errorDocument: asString(hosting.errorDocument),
1385
- }),
1386
- },
1387
- };
1388
- })()),
1389
- ...(asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS) && {
1390
- backupVaultTier: asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS),
1391
- }),
1392
- ...(asBoolean(s3.config.versioned) !== undefined && {
1393
- versioned: asBoolean(s3.config.versioned),
1394
- }),
1395
- ...(asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES) && {
1396
- encryption: asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES),
1397
- }),
1398
- ...(asString(s3.config.kmsKeyArn) && {
1399
- kmsKeyArn: asString(s3.config.kmsKeyArn),
1400
- }),
1401
- ...(Array.isArray(s3.config.cors) &&
1402
- s3.config.cors.length > 0 && {
1403
- cors: s3.config.cors.filter(isParsedObject).map((rule) => ({
1404
- allowedOrigins: Array.isArray(rule.allowedOrigins)
1405
- ? rule.allowedOrigins.filter((o) => typeof o === "string")
1406
- : [],
1407
- allowedMethods: Array.isArray(rule.allowedMethods)
1408
- ? rule.allowedMethods.filter((m) => typeof m === "string")
1409
- : [],
1410
- })),
1411
- }),
1412
- ...(isParsedObject(s3.config.deployment) &&
1413
- (() => {
1414
- const deployment = s3.config.deployment;
1415
- return {
1416
- deployment: {
1417
- source: asString(deployment.source) ?? "",
1418
- ...(asBoolean(deployment.prune) !== undefined && {
1419
- prune: asBoolean(deployment.prune),
1420
- }),
1421
- ...(isParsedObject(deployment.cacheControl) &&
1422
- (() => {
1423
- const cache = deployment.cacheControl;
1424
- return {
1425
- cacheControl: {
1426
- ...(asNumber(cache.maxAge) !== undefined && {
1427
- maxAge: asNumber(cache.maxAge),
1428
- }),
1429
- ...(asBoolean(cache.immutable) !== undefined && {
1430
- immutable: asBoolean(cache.immutable),
1431
- }),
1432
- },
1433
- };
1434
- })()),
1435
- },
1436
- };
1437
- })()),
1438
- ...(asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS) && {
1439
- stackPlacement: asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS),
1440
- }),
1441
- ...(s3.variableName && { variableName: s3.variableName }),
1442
- ...(() => {
1443
- const S3_KNOWN_KEYS = new Set([
1444
- "bucketName",
1445
- "publicReadAccess",
1446
- "websiteHosting",
1447
- "backupVaultTier",
1448
- "versioned",
1449
- "encryption",
1450
- "kmsKeyArn",
1451
- "cors",
1452
- "deployment",
1453
- "stackPlacement",
1454
- ]);
1455
- const extras = captureExtraProperties(s3.config, S3_KNOWN_KEYS);
1456
- return extras.length > 0 ? { extraProperties: extras } : {};
1457
- })(),
1458
- }));
1459
- }
1460
- function applyPatternConfig(plan, parsed) {
1461
- if (!parsed.patternResources || parsed.patternResources.length === 0)
1462
- return;
1463
- const patternResource = parsed.patternResources[0];
1464
- if (patternResource.type !== "payload" && patternResource.type !== "nextjs")
1465
- return;
1466
- plan.pattern = patternResource.type;
1467
- plan.patternConfig = {
1468
- type: patternResource.type,
1469
- name: patternResource.config.name,
1470
- domain: patternResource.config.domain,
1471
- database: patternResource.config.database,
1472
- compute: patternResource.config.compute,
1473
- storage: patternResource.config.storage,
1474
- messaging: patternResource.config.messaging,
1475
- cdn: patternResource.config.cdn,
1476
- environment: patternResource.config.environment,
1477
- };
1478
- }
1479
- function applyTagsAndOwner(plan, tags) {
1480
- if (Object.keys(tags).length > 0) {
1481
- plan.tags = { ...tags };
1482
- }
1483
- if (tags[COST_ALLOCATION_TAG]) {
1484
- plan.owner = tags[COST_ALLOCATION_TAG];
1485
- }
1486
- }
1487
- function convertAdditionalNetworks(parsed) {
1488
- if (!parsed.networkResources || parsed.networkResources.length === 0)
1489
- return undefined;
1490
- return parsed.networkResources.map((network) => ({
1491
- name: network.name,
1492
- ...buildNetworkFields(network.config),
1493
- }));
209
+ return additional.length > 0 ? additional : undefined;
1494
210
  }
1495
211
  export function convertToResourcePlan(parsed, appName, options) {
1496
212
  const plan = {
@@ -1499,26 +215,24 @@ export function convertToResourcePlan(parsed, appName, options) {
1499
215
  database: [],
1500
216
  s3: [],
1501
217
  compute: [],
1502
- importedResources: [],
1503
218
  };
1504
- applyPatternConfig(plan, parsed);
219
+ applyPatternConfig(plan, parsed.patternResources);
1505
220
  if (parsed.vpcId)
1506
221
  plan.vpcId = parsed.vpcId;
1507
- plan.network = convertNetworkConfig(parsed);
1508
- plan.backup = convertBackupConfig(parsed);
1509
- plan.tunnel = convertTunnelConfig(parsed);
1510
- plan.additionalNetworks = convertAdditionalNetworks(parsed);
222
+ plan.network = convertNetworkConfig(parsed.network);
223
+ plan.backup = convertBackupConfig(parsed.backup);
224
+ plan.tunnel = convertTunnelConfig(parsed.tunnel);
225
+ plan.additionalNetworks = convertAdditionalNetworks(parsed.networkResources);
1511
226
  applyTagsAndOwner(plan, parsed.tags);
1512
- plan.database = convertDatabaseResources(parsed);
1513
- plan.dynamodb = convertDynamoDBResources(parsed);
1514
- plan.sqs = convertSQSResources(parsed);
1515
- plan.cdn = convertCDNResource(parsed);
1516
- plan.s3 = convertS3Resources(parsed);
1517
- plan.compute = convertComputeResources(parsed, plan);
227
+ plan.database = convertDatabaseResources(parsed.databaseResources);
228
+ plan.dynamodb = convertDynamoDBResources(parsed.dynamodbResources);
229
+ plan.sqs = convertSQSResources(parsed.sqsResources);
230
+ plan.cdn = convertCDNResource(parsed.cdnResource, parsed.s3Resources, parsed.computeResources);
231
+ plan.s3 = convertS3Resources(parsed.s3Resources);
232
+ plan.compute = convertComputeResources(parsed.computeResources, parsed.databaseResources, parsed.s3Resources, plan);
1518
233
  plan.additionalManagedImports = computeAdditionalManagedImports(parsed.imports);
1519
- // Round-trip operations (parse modify regenerate) skip validation
234
+ // Round-trip operations (parse -> modify -> regenerate) skip validation
1520
235
  // to tolerate existing apps with non-standard names (e.g. PascalCase).
1521
- // TODO: Remove once all apps use lowercase-first names (AppNameSchema).
1522
236
  if (options?.skipValidation) {
1523
237
  return plan;
1524
238
  }
@@ -1535,7 +249,6 @@ export function convertToResourcePlan(parsed, appName, options) {
1535
249
  }
1536
250
  /**
1537
251
  * Classify all top-level statements in an infrastructure file.
1538
- * This identifies which statements are managed by Fjall vs custom user code.
1539
252
  */
1540
253
  export function classifyStatements(sourceFile) {
1541
254
  const classifications = [];
@@ -1552,19 +265,16 @@ export function classifyStatements(sourceFile) {
1552
265
  });
1553
266
  continue;
1554
267
  }
1555
- // Variable statements (app init, S3 buckets, etc.)
1556
268
  if (ts.isVariableStatement(statement)) {
1557
269
  const classification = classifyVariableStatement(statement, startPos, endPos);
1558
270
  classifications.push(classification);
1559
271
  continue;
1560
272
  }
1561
- // Expression statements (app.addDatabase, app.addTags, etc.)
1562
273
  if (ts.isExpressionStatement(statement)) {
1563
274
  const classification = classifyExpressionStatement(statement, startPos, endPos);
1564
275
  classifications.push(classification);
1565
276
  continue;
1566
277
  }
1567
- // Any other statement is custom code
1568
278
  classifications.push({
1569
279
  type: "custom",
1570
280
  node: statement,
@@ -1575,74 +285,11 @@ export function classifyStatements(sourceFile) {
1575
285
  }
1576
286
  return classifications;
1577
287
  }
1578
- /** Imports that the generator always handles — don't preserve as "additional" */
1579
- const KNOWN_FJALL_IMPORTS = new Set([
1580
- "App",
1581
- "Architecture",
1582
- "DatabaseFactory",
1583
- "StorageFactory",
1584
- "ComputeFactory",
1585
- "getConfig",
1586
- "MessagingFactory",
1587
- "CdnFactory",
1588
- "Code",
1589
- "Runtime",
1590
- "FunctionUrlAuthType",
1591
- "NetworkFactory",
1592
- "PatternFactory",
1593
- ]);
1594
- /** Check if a module specifier belongs to a managed (Fjall/CDK) package */
1595
- function isManagedModuleSpecifier(specifier) {
1596
- if (specifier.startsWith("@fjall/"))
1597
- return true;
1598
- if (specifier === "aws-cdk-lib" || specifier.startsWith("aws-cdk-lib/"))
1599
- return true;
1600
- if (specifier === "constructs")
1601
- return true;
1602
- return false;
1603
- }
1604
- /**
1605
- * Compute additional managed imports that the generator doesn't handle.
1606
- * For @fjall/components-infrastructure: keep named imports NOT in KNOWN_FJALL_IMPORTS.
1607
- * For other managed modules (aws-cdk-lib/*, constructs): preserve entire import.
1608
- */
1609
- function computeAdditionalManagedImports(imports) {
1610
- const additional = [];
1611
- for (const imp of imports) {
1612
- if (!isManagedModuleSpecifier(imp.moduleSpecifier))
1613
- continue;
1614
- if (imp.moduleSpecifier === "@fjall/components-infrastructure" ||
1615
- imp.moduleSpecifier === "@fjall/infrastructure") {
1616
- const extraNames = imp.namedImports.filter((n) => !KNOWN_FJALL_IMPORTS.has(n));
1617
- if (extraNames.length > 0) {
1618
- additional.push({
1619
- moduleSpecifier: imp.moduleSpecifier,
1620
- namedImports: extraNames,
1621
- defaultImport: imp.defaultImport,
1622
- });
1623
- }
1624
- }
1625
- else {
1626
- // aws-cdk-lib/*, constructs — preserve entire import
1627
- if (imp.namedImports.length > 0 || imp.defaultImport) {
1628
- additional.push(imp);
1629
- }
1630
- }
1631
- }
1632
- return additional.length > 0 ? additional : undefined;
1633
- }
1634
- /**
1635
- * Check if an import is managed by Fjall.
1636
- * Managed imports include: @fjall/infrastructure, aws-cdk-lib/*, constructs
1637
- */
1638
288
  function isManagedImport(node) {
1639
289
  if (!ts.isStringLiteral(node.moduleSpecifier))
1640
290
  return false;
1641
291
  return isManagedModuleSpecifier(node.moduleSpecifier.text);
1642
292
  }
1643
- /**
1644
- * Classify a variable statement (const/let/var declarations).
1645
- */
1646
293
  function classifyVariableStatement(statement, startPos, endPos) {
1647
294
  for (const declaration of statement.declarationList.declarations) {
1648
295
  if (!declaration.initializer)
@@ -1706,9 +353,6 @@ function classifyVariableStatement(statement, startPos, endPos) {
1706
353
  isManaged: false,
1707
354
  };
1708
355
  }
1709
- /**
1710
- * Classify an expression statement (method calls like app.addDatabase).
1711
- */
1712
356
  function classifyExpressionStatement(statement, startPos, endPos) {
1713
357
  const expr = statement.expression;
1714
358
  if (ts.isCallExpression(expr)) {
@@ -1722,9 +366,6 @@ function classifyExpressionStatement(statement, startPos, endPos) {
1722
366
  isManaged: false,
1723
367
  };
1724
368
  }
1725
- /**
1726
- * Classify a call expression (e.g., app.addDatabase, app.addTags).
1727
- */
1728
369
  function classifyCallExpression(call, statement, startPos, endPos) {
1729
370
  const expression = call.expression;
1730
371
  if (ts.isPropertyAccessExpression(expression) &&
@@ -1769,10 +410,6 @@ function classifyCallExpression(call, statement, startPos, endPos) {
1769
410
  isManaged: false,
1770
411
  };
1771
412
  }
1772
- /**
1773
- * Extract the resource name from a Factory.build() call.
1774
- * e.g., app.addDatabase(DatabaseFactory.build("MyDatabase", {...}))
1775
- */
1776
413
  function extractResourceNameFromFactoryCall(addCall, factoryName) {
1777
414
  if (addCall.arguments.length === 0)
1778
415
  return undefined;
@@ -1815,7 +452,6 @@ function determineCustomBlockPosition(lastManaged) {
1815
452
  }
1816
453
  /**
1817
454
  * Extract custom code blocks from an infrastructure file.
1818
- * Returns an array of custom code blocks with their positions relative to managed resources.
1819
455
  */
1820
456
  export function extractCustomCodeBlocks(sourceFile, precomputedClassifications) {
1821
457
  const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
@@ -1842,9 +478,6 @@ export function extractCustomCodeBlocks(sourceFile, precomputedClassifications)
1842
478
  }
1843
479
  return customBlocks;
1844
480
  }
1845
- /**
1846
- * Get the full statement text including any leading comments and whitespace.
1847
- */
1848
481
  function getStatementTextWithComments(sourceFile, sourceText, node) {
1849
482
  const fullStart = node.getFullStart();
1850
483
  const end = node.getEnd();
@@ -1853,9 +486,6 @@ function getStatementTextWithComments(sourceFile, sourceText, node) {
1853
486
  const startIndex = lines.findIndex((line) => line.trim() !== "");
1854
487
  return lines.slice(startIndex === -1 ? 0 : startIndex).join("\n");
1855
488
  }
1856
- /**
1857
- * Extract leading comments for a node.
1858
- */
1859
489
  function extractLeadingComments(sourceFile, sourceText, node) {
1860
490
  const comments = [];
1861
491
  const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -1869,7 +499,6 @@ function extractLeadingComments(sourceFile, sourceText, node) {
1869
499
  }
1870
500
  /**
1871
501
  * Find the position information for a specific managed resource.
1872
- * Useful for surgical updates.
1873
502
  */
1874
503
  export function findManagedResourcePosition(sourceFile, resourceType, resourceName, precomputedClassifications) {
1875
504
  const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
@@ -1887,7 +516,6 @@ export function findManagedResourcePosition(sourceFile, resourceType, resourceNa
1887
516
  }
1888
517
  /**
1889
518
  * Get the last managed statement of a specific type.
1890
- * Used to determine insertion points for new resources.
1891
519
  */
1892
520
  export function getLastManagedStatementOfType(sourceFile, type, precomputedClassifications) {
1893
521
  const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
@@ -1901,7 +529,6 @@ export function getLastManagedStatementOfType(sourceFile, type, precomputedClass
1901
529
  }
1902
530
  /**
1903
531
  * Get all managed resources grouped by type.
1904
- * Useful for understanding the structure of an infrastructure file.
1905
532
  */
1906
533
  export function getManagedResourcesByType(sourceFile, precomputedClassifications) {
1907
534
  const classifications = precomputedClassifications ?? classifyStatements(sourceFile);