@fjall/generator 0.88.4

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 (57) hide show
  1. package/dist/src/ast/astComputeParser.d.ts +4 -0
  2. package/dist/src/ast/astComputeParser.js +427 -0
  3. package/dist/src/ast/astInfrastructureParser.d.ts +357 -0
  4. package/dist/src/ast/astInfrastructureParser.js +1925 -0
  5. package/dist/src/ast/astSurgicalModification.d.ts +47 -0
  6. package/dist/src/ast/astSurgicalModification.js +400 -0
  7. package/dist/src/ast/index.d.ts +2 -0
  8. package/dist/src/ast/index.js +2 -0
  9. package/dist/src/aws/regions.d.ts +30 -0
  10. package/dist/src/aws/regions.js +254 -0
  11. package/dist/src/generation/common.d.ts +86 -0
  12. package/dist/src/generation/common.js +187 -0
  13. package/dist/src/generation/compute.d.ts +6 -0
  14. package/dist/src/generation/compute.js +547 -0
  15. package/dist/src/generation/database.d.ts +54 -0
  16. package/dist/src/generation/database.js +201 -0
  17. package/dist/src/generation/index.d.ts +12 -0
  18. package/dist/src/generation/index.js +18 -0
  19. package/dist/src/generation/infrastructure.d.ts +44 -0
  20. package/dist/src/generation/infrastructure.js +389 -0
  21. package/dist/src/generation/storage.d.ts +23 -0
  22. package/dist/src/generation/storage.js +174 -0
  23. package/dist/src/generation/storageConnections.d.ts +37 -0
  24. package/dist/src/generation/storageConnections.js +71 -0
  25. package/dist/src/index.d.ts +10 -0
  26. package/dist/src/index.js +19 -0
  27. package/dist/src/planning/index.d.ts +1 -0
  28. package/dist/src/planning/index.js +1 -0
  29. package/dist/src/planning/resourcePlanning.d.ts +58 -0
  30. package/dist/src/planning/resourcePlanning.js +216 -0
  31. package/dist/src/presets/index.d.ts +3 -0
  32. package/dist/src/presets/index.js +3 -0
  33. package/dist/src/presets/patternTierPresets.d.ts +93 -0
  34. package/dist/src/presets/patternTierPresets.js +131 -0
  35. package/dist/src/presets/storagePresets.d.ts +11 -0
  36. package/dist/src/presets/storagePresets.js +36 -0
  37. package/dist/src/presets/tierPresets.d.ts +59 -0
  38. package/dist/src/presets/tierPresets.js +384 -0
  39. package/dist/src/presets/tierTypes.d.ts +301 -0
  40. package/dist/src/presets/tierTypes.js +7 -0
  41. package/dist/src/schemas/constants.d.ts +74 -0
  42. package/dist/src/schemas/constants.js +208 -0
  43. package/dist/src/schemas/index.d.ts +3 -0
  44. package/dist/src/schemas/index.js +3 -0
  45. package/dist/src/schemas/instanceTypeArchitecture.d.ts +35 -0
  46. package/dist/src/schemas/instanceTypeArchitecture.js +75 -0
  47. package/dist/src/schemas/resourceSchemas.d.ts +3534 -0
  48. package/dist/src/schemas/resourceSchemas.js +2015 -0
  49. package/dist/src/types/Result.d.ts +19 -0
  50. package/dist/src/types/Result.js +31 -0
  51. package/dist/src/util/errorUtils.d.ts +2 -0
  52. package/dist/src/util/errorUtils.js +15 -0
  53. package/dist/src/validation/patterns.d.ts +300 -0
  54. package/dist/src/validation/patterns.js +360 -0
  55. package/dist/src/version.d.ts +1 -0
  56. package/dist/src/version.js +1 -0
  57. package/package.json +32 -0
@@ -0,0 +1,1925 @@
1
+ import * as ts from "typescript";
2
+ import { ApplicationResourcePlanSchema, getZodErrorMessage, S3_STACK_PLACEMENTS, } from "../schemas/resourceSchemas.js";
3
+ import { z } from "zod";
4
+ import { constIncludes, S3_ENCRYPTION_TYPES, BACKUP_VAULT_TIERS, } from "../schemas/constants.js";
5
+ 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"];
213
+ export function parseInfrastructure(content, options) {
214
+ const sourceFile = ts.createSourceFile("infrastructure.ts", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
215
+ const imports = extractImports(sourceFile);
216
+ const { appVariableName, appName } = findAppVariable(sourceFile);
217
+ const vpcId = findVpcId(sourceFile);
218
+ const network = findNetworkConfig(sourceFile);
219
+ const backup = findBackupConfig(sourceFile);
220
+ const tunnel = findTunnelConfig(sourceFile);
221
+ const networkResources = findNetworkResources(sourceFile);
222
+ const allDatabaseResources = findDatabaseResources(sourceFile);
223
+ const { databases: databaseResources, dynamodb: dynamodbResources } = splitDynamoDBFromDatabases(allDatabaseResources);
224
+ const sqsResources = findSQSResources(sourceFile);
225
+ const cdnResource = findCDNResource(sourceFile);
226
+ const s3Resources = [
227
+ ...findS3Resources(sourceFile),
228
+ ...findS3FactoryResources(sourceFile),
229
+ ];
230
+ const computeResources = findComputeResources(sourceFile);
231
+ const patternResources = findPatternResources(sourceFile);
232
+ const tags = extractTags(sourceFile);
233
+ const result = {
234
+ sourceFile,
235
+ imports,
236
+ appVariableName,
237
+ appName,
238
+ vpcId,
239
+ network,
240
+ backup,
241
+ tunnel,
242
+ networkResources,
243
+ databaseResources,
244
+ dynamodbResources,
245
+ sqsResources,
246
+ cdnResource,
247
+ s3Resources,
248
+ computeResources,
249
+ patternResources,
250
+ tags,
251
+ };
252
+ if (options?.extractCustomCode) {
253
+ result.customCodeBlocks = extractCustomCodeBlocks(sourceFile);
254
+ result.classifiedStatements = classifyStatements(sourceFile);
255
+ }
256
+ return result;
257
+ }
258
+ function extractImports(sourceFile) {
259
+ const imports = [];
260
+ ts.forEachChild(sourceFile, (node) => {
261
+ if (ts.isImportDeclaration(node)) {
262
+ if (!ts.isStringLiteral(node.moduleSpecifier))
263
+ return;
264
+ const moduleSpecifier = node.moduleSpecifier.text;
265
+ const importInfo = {
266
+ moduleSpecifier,
267
+ namedImports: [],
268
+ };
269
+ if (node.importClause) {
270
+ if (node.importClause.name) {
271
+ importInfo.defaultImport = node.importClause.name.text;
272
+ }
273
+ if (node.importClause.namedBindings &&
274
+ ts.isNamedImports(node.importClause.namedBindings)) {
275
+ importInfo.namedImports =
276
+ node.importClause.namedBindings.elements.map((element) => element.name.text);
277
+ }
278
+ }
279
+ imports.push(importInfo);
280
+ }
281
+ });
282
+ return imports;
283
+ }
284
+ /** Resolve the app name from the first argument of App.getApp(). */
285
+ function resolveAppName(sourceFile, arg) {
286
+ if (ts.isStringLiteral(arg))
287
+ return arg.text;
288
+ if (ts.isIdentifier(arg))
289
+ return findVariableValue(sourceFile, arg.text) ?? "";
290
+ if (ts.isPropertyAccessExpression(arg))
291
+ return evaluatePropertyAccess(sourceFile, arg) ?? "";
292
+ return "";
293
+ }
294
+ function findAppVariable(sourceFile) {
295
+ return (findFirstInAst(sourceFile, (node) => {
296
+ if (!ts.isVariableStatement(node))
297
+ return undefined;
298
+ for (const declaration of node.declarationList.declarations) {
299
+ if (!declaration.initializer ||
300
+ !isAppGetAppCall(declaration.initializer))
301
+ continue;
302
+ const varName = ts.isIdentifier(declaration.name)
303
+ ? declaration.name.text
304
+ : "";
305
+ const appName = declaration.initializer.arguments.length > 0
306
+ ? resolveAppName(sourceFile, declaration.initializer.arguments[0])
307
+ : "";
308
+ return { appVariableName: varName, appName };
309
+ }
310
+ return undefined;
311
+ }) ?? { appVariableName: "", appName: "" });
312
+ }
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
+ }
1046
+ function extractTags(sourceFile) {
1047
+ const tags = {};
1048
+ function visit(node) {
1049
+ if (ts.isCallExpression(node) &&
1050
+ ts.isPropertyAccessExpression(node.expression) &&
1051
+ node.expression.name.text === "addTags" &&
1052
+ ts.isIdentifier(node.expression.expression) &&
1053
+ node.arguments.length > 0) {
1054
+ const tagsArg = node.arguments[0];
1055
+ if (ts.isObjectLiteralExpression(tagsArg)) {
1056
+ const parsedTags = parseObjectLiteral(tagsArg);
1057
+ for (const [key, value] of Object.entries(parsedTags)) {
1058
+ if (typeof value === "string") {
1059
+ tags[key] = value;
1060
+ }
1061
+ else if (isExpressionRef(value)) {
1062
+ // process.env tag expressions can't be statically resolved; use placeholder
1063
+ tags[key] = "default-value";
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ ts.forEachChild(node, visit);
1069
+ }
1070
+ visit(sourceFile);
1071
+ return tags;
1072
+ }
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 };
1105
+ }
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;
1118
+ }
1119
+ return result;
1120
+ }
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 };
1130
+ }
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);
1140
+ }
1141
+ 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 };
1315
+ }
1316
+ }
1317
+ }
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
+ }));
1494
+ }
1495
+ export function convertToResourcePlan(parsed, appName, options) {
1496
+ const plan = {
1497
+ appName,
1498
+ type: "standard",
1499
+ database: [],
1500
+ s3: [],
1501
+ compute: [],
1502
+ importedResources: [],
1503
+ };
1504
+ applyPatternConfig(plan, parsed);
1505
+ if (parsed.vpcId)
1506
+ plan.vpcId = parsed.vpcId;
1507
+ plan.network = convertNetworkConfig(parsed);
1508
+ plan.backup = convertBackupConfig(parsed);
1509
+ plan.tunnel = convertTunnelConfig(parsed);
1510
+ plan.additionalNetworks = convertAdditionalNetworks(parsed);
1511
+ 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);
1518
+ plan.additionalManagedImports = computeAdditionalManagedImports(parsed.imports);
1519
+ // Round-trip operations (parse → modify → regenerate) skip validation
1520
+ // to tolerate existing apps with non-standard names (e.g. PascalCase).
1521
+ // TODO: Remove once all apps use lowercase-first names (AppNameSchema).
1522
+ if (options?.skipValidation) {
1523
+ return plan;
1524
+ }
1525
+ try {
1526
+ return ApplicationResourcePlanSchema.parse(plan);
1527
+ }
1528
+ catch (error) {
1529
+ if (error instanceof z.ZodError) {
1530
+ const errorMessage = getZodErrorMessage(error);
1531
+ throw new Error(`Invalid infrastructure configuration:\n${errorMessage}`);
1532
+ }
1533
+ throw error;
1534
+ }
1535
+ }
1536
+ /**
1537
+ * Classify all top-level statements in an infrastructure file.
1538
+ * This identifies which statements are managed by Fjall vs custom user code.
1539
+ */
1540
+ export function classifyStatements(sourceFile) {
1541
+ const classifications = [];
1542
+ for (const statement of sourceFile.statements) {
1543
+ const startPos = statement.getFullStart();
1544
+ const endPos = statement.getEnd();
1545
+ if (ts.isImportDeclaration(statement)) {
1546
+ classifications.push({
1547
+ type: "import",
1548
+ node: statement,
1549
+ startPos,
1550
+ endPos,
1551
+ isManaged: isManagedImport(statement),
1552
+ });
1553
+ continue;
1554
+ }
1555
+ // Variable statements (app init, S3 buckets, etc.)
1556
+ if (ts.isVariableStatement(statement)) {
1557
+ const classification = classifyVariableStatement(statement, startPos, endPos);
1558
+ classifications.push(classification);
1559
+ continue;
1560
+ }
1561
+ // Expression statements (app.addDatabase, app.addTags, etc.)
1562
+ if (ts.isExpressionStatement(statement)) {
1563
+ const classification = classifyExpressionStatement(statement, startPos, endPos);
1564
+ classifications.push(classification);
1565
+ continue;
1566
+ }
1567
+ // Any other statement is custom code
1568
+ classifications.push({
1569
+ type: "custom",
1570
+ node: statement,
1571
+ startPos,
1572
+ endPos,
1573
+ isManaged: false,
1574
+ });
1575
+ }
1576
+ return classifications;
1577
+ }
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
+ function isManagedImport(node) {
1639
+ if (!ts.isStringLiteral(node.moduleSpecifier))
1640
+ return false;
1641
+ return isManagedModuleSpecifier(node.moduleSpecifier.text);
1642
+ }
1643
+ /**
1644
+ * Classify a variable statement (const/let/var declarations).
1645
+ */
1646
+ function classifyVariableStatement(statement, startPos, endPos) {
1647
+ for (const declaration of statement.declarationList.declarations) {
1648
+ if (!declaration.initializer)
1649
+ continue;
1650
+ if (isAppGetAppCall(declaration.initializer)) {
1651
+ return {
1652
+ type: "app-init",
1653
+ node: statement,
1654
+ resourceName: ts.isIdentifier(declaration.name)
1655
+ ? declaration.name.text
1656
+ : undefined,
1657
+ startPos,
1658
+ endPos,
1659
+ isManaged: true,
1660
+ };
1661
+ }
1662
+ if (ts.isIdentifier(declaration.name) &&
1663
+ declaration.name.text === "appName" &&
1664
+ ts.isStringLiteral(declaration.initializer)) {
1665
+ return {
1666
+ type: "app-init",
1667
+ node: statement,
1668
+ startPos,
1669
+ endPos,
1670
+ isManaged: true,
1671
+ };
1672
+ }
1673
+ if (ts.isNewExpression(declaration.initializer)) {
1674
+ const newExpr = declaration.initializer;
1675
+ if (ts.isIdentifier(newExpr.expression)) {
1676
+ const className = newExpr.expression.text;
1677
+ if (S3_BUCKET_CLASSES.has(className)) {
1678
+ const resourceName = newExpr.arguments &&
1679
+ newExpr.arguments.length >= 2 &&
1680
+ ts.isStringLiteral(newExpr.arguments[1])
1681
+ ? newExpr.arguments[1].text
1682
+ : undefined;
1683
+ return {
1684
+ type: "storage",
1685
+ node: statement,
1686
+ resourceName,
1687
+ startPos,
1688
+ endPos,
1689
+ isManaged: true,
1690
+ };
1691
+ }
1692
+ }
1693
+ }
1694
+ if (ts.isCallExpression(declaration.initializer)) {
1695
+ const classification = classifyCallExpression(declaration.initializer, statement, startPos, endPos);
1696
+ if (classification.type !== "custom") {
1697
+ return classification;
1698
+ }
1699
+ }
1700
+ }
1701
+ return {
1702
+ type: "custom",
1703
+ node: statement,
1704
+ startPos,
1705
+ endPos,
1706
+ isManaged: false,
1707
+ };
1708
+ }
1709
+ /**
1710
+ * Classify an expression statement (method calls like app.addDatabase).
1711
+ */
1712
+ function classifyExpressionStatement(statement, startPos, endPos) {
1713
+ const expr = statement.expression;
1714
+ if (ts.isCallExpression(expr)) {
1715
+ return classifyCallExpression(expr, statement, startPos, endPos);
1716
+ }
1717
+ return {
1718
+ type: "custom",
1719
+ node: statement,
1720
+ startPos,
1721
+ endPos,
1722
+ isManaged: false,
1723
+ };
1724
+ }
1725
+ /**
1726
+ * Classify a call expression (e.g., app.addDatabase, app.addTags).
1727
+ */
1728
+ function classifyCallExpression(call, statement, startPos, endPos) {
1729
+ const expression = call.expression;
1730
+ if (ts.isPropertyAccessExpression(expression) &&
1731
+ ts.isIdentifier(expression.expression)) {
1732
+ const methodName = expression.name.text;
1733
+ if (methodName === "addTags") {
1734
+ return {
1735
+ type: "tags",
1736
+ node: statement,
1737
+ startPos,
1738
+ endPos,
1739
+ isManaged: true,
1740
+ };
1741
+ }
1742
+ const factoryMethods = {
1743
+ addDatabase: { type: "database", factory: "DatabaseFactory" },
1744
+ addCompute: { type: "compute", factory: "ComputeFactory" },
1745
+ addNetwork: { type: "network", factory: "NetworkFactory" },
1746
+ addMessaging: { type: "messaging", factory: "MessagingFactory" },
1747
+ addCdn: { type: "cdn", factory: "CdnFactory" },
1748
+ addPattern: { type: "pattern", factory: "PatternFactory" },
1749
+ addStorage: { type: "storage", factory: "StorageFactory" },
1750
+ };
1751
+ const factoryMethod = factoryMethods[methodName];
1752
+ if (factoryMethod) {
1753
+ const resourceName = extractResourceNameFromFactoryCall(call, factoryMethod.factory);
1754
+ return {
1755
+ type: factoryMethod.type,
1756
+ node: statement,
1757
+ resourceName,
1758
+ startPos,
1759
+ endPos,
1760
+ isManaged: true,
1761
+ };
1762
+ }
1763
+ }
1764
+ return {
1765
+ type: "custom",
1766
+ node: statement,
1767
+ startPos,
1768
+ endPos,
1769
+ isManaged: false,
1770
+ };
1771
+ }
1772
+ /**
1773
+ * Extract the resource name from a Factory.build() call.
1774
+ * e.g., app.addDatabase(DatabaseFactory.build("MyDatabase", {...}))
1775
+ */
1776
+ function extractResourceNameFromFactoryCall(addCall, factoryName) {
1777
+ if (addCall.arguments.length === 0)
1778
+ return undefined;
1779
+ const factoryArg = addCall.arguments[0];
1780
+ if (!ts.isCallExpression(factoryArg))
1781
+ return undefined;
1782
+ const factoryExpr = factoryArg.expression;
1783
+ if (!ts.isPropertyAccessExpression(factoryExpr) ||
1784
+ factoryExpr.name.text !== "build" ||
1785
+ !ts.isIdentifier(factoryExpr.expression) ||
1786
+ factoryExpr.expression.text !== factoryName) {
1787
+ return undefined;
1788
+ }
1789
+ if (factoryArg.arguments.length === 0)
1790
+ return undefined;
1791
+ const nameArg = factoryArg.arguments[0];
1792
+ if (ts.isStringLiteral(nameArg)) {
1793
+ return nameArg.text;
1794
+ }
1795
+ return undefined;
1796
+ }
1797
+ const POSITION_BY_MANAGED_TYPE = {
1798
+ import: "after-imports",
1799
+ "app-init": "after-app-init",
1800
+ tags: "after-tags",
1801
+ };
1802
+ function determineCustomBlockPosition(lastManaged) {
1803
+ if (!lastManaged)
1804
+ return { position: "before-imports" };
1805
+ const knownPosition = POSITION_BY_MANAGED_TYPE[lastManaged.type];
1806
+ if (knownPosition)
1807
+ return { position: knownPosition };
1808
+ return {
1809
+ position: "after-resource",
1810
+ afterManagedResource: {
1811
+ type: lastManaged.type,
1812
+ name: lastManaged.resourceName,
1813
+ },
1814
+ };
1815
+ }
1816
+ /**
1817
+ * Extract custom code blocks from an infrastructure file.
1818
+ * Returns an array of custom code blocks with their positions relative to managed resources.
1819
+ */
1820
+ export function extractCustomCodeBlocks(sourceFile, precomputedClassifications) {
1821
+ const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
1822
+ const customBlocks = [];
1823
+ const sourceText = sourceFile.getFullText();
1824
+ let lastManagedStatement = null;
1825
+ for (const current of classifications) {
1826
+ if (current.type === "custom" && !current.isManaged) {
1827
+ const { position, afterManagedResource } = determineCustomBlockPosition(lastManagedStatement);
1828
+ const fullText = getStatementTextWithComments(sourceFile, sourceText, current.node);
1829
+ const leadingComments = extractLeadingComments(sourceFile, sourceText, current.node);
1830
+ const lineAndChar = sourceFile.getLineAndCharacterOfPosition(current.node.getStart());
1831
+ customBlocks.push({
1832
+ sourceText: fullText,
1833
+ position,
1834
+ afterManagedResource,
1835
+ originalLine: lineAndChar.line + 1,
1836
+ leadingComments: leadingComments.length > 0 ? leadingComments : undefined,
1837
+ });
1838
+ }
1839
+ else if (current.isManaged) {
1840
+ lastManagedStatement = current;
1841
+ }
1842
+ }
1843
+ return customBlocks;
1844
+ }
1845
+ /**
1846
+ * Get the full statement text including any leading comments and whitespace.
1847
+ */
1848
+ function getStatementTextWithComments(sourceFile, sourceText, node) {
1849
+ const fullStart = node.getFullStart();
1850
+ const end = node.getEnd();
1851
+ const text = sourceText.slice(fullStart, end);
1852
+ const lines = text.split("\n");
1853
+ const startIndex = lines.findIndex((line) => line.trim() !== "");
1854
+ return lines.slice(startIndex === -1 ? 0 : startIndex).join("\n");
1855
+ }
1856
+ /**
1857
+ * Extract leading comments for a node.
1858
+ */
1859
+ function extractLeadingComments(sourceFile, sourceText, node) {
1860
+ const comments = [];
1861
+ const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
1862
+ if (commentRanges) {
1863
+ for (const range of commentRanges) {
1864
+ const commentText = sourceText.slice(range.pos, range.end);
1865
+ comments.push(commentText);
1866
+ }
1867
+ }
1868
+ return comments;
1869
+ }
1870
+ /**
1871
+ * Find the position information for a specific managed resource.
1872
+ * Useful for surgical updates.
1873
+ */
1874
+ export function findManagedResourcePosition(sourceFile, resourceType, resourceName, precomputedClassifications) {
1875
+ const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
1876
+ for (const classification of classifications) {
1877
+ if (classification.type === resourceType &&
1878
+ classification.resourceName === resourceName) {
1879
+ return {
1880
+ startPos: classification.startPos,
1881
+ endPos: classification.endPos,
1882
+ node: classification.node,
1883
+ };
1884
+ }
1885
+ }
1886
+ return null;
1887
+ }
1888
+ /**
1889
+ * Get the last managed statement of a specific type.
1890
+ * Used to determine insertion points for new resources.
1891
+ */
1892
+ export function getLastManagedStatementOfType(sourceFile, type, precomputedClassifications) {
1893
+ const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
1894
+ let lastOfType = null;
1895
+ for (const classification of classifications) {
1896
+ if (classification.type === type && classification.isManaged) {
1897
+ lastOfType = classification;
1898
+ }
1899
+ }
1900
+ return lastOfType;
1901
+ }
1902
+ /**
1903
+ * Get all managed resources grouped by type.
1904
+ * Useful for understanding the structure of an infrastructure file.
1905
+ */
1906
+ export function getManagedResourcesByType(sourceFile, precomputedClassifications) {
1907
+ const classifications = precomputedClassifications ?? classifyStatements(sourceFile);
1908
+ const byType = {
1909
+ import: [],
1910
+ "app-init": [],
1911
+ tags: [],
1912
+ database: [],
1913
+ compute: [],
1914
+ storage: [],
1915
+ network: [],
1916
+ messaging: [],
1917
+ cdn: [],
1918
+ pattern: [],
1919
+ custom: [],
1920
+ };
1921
+ for (const classification of classifications) {
1922
+ byType[classification.type].push(classification);
1923
+ }
1924
+ return byType;
1925
+ }