@fjall/generator 0.89.5 → 0.94.0

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 (168) hide show
  1. package/LICENSE +50 -21
  2. package/README.md +28 -0
  3. package/dist/.minified +1 -0
  4. package/dist/src/ast/astCdnParser.d.ts +5 -0
  5. package/dist/src/ast/astCdnParser.js +1 -114
  6. package/dist/src/ast/astCommonParser.d.ts +6 -17
  7. package/dist/src/ast/astCommonParser.js +1 -351
  8. package/dist/src/ast/astComputeConnectionParser.d.ts +18 -0
  9. package/dist/src/ast/astComputeConnectionParser.js +1 -0
  10. package/dist/src/ast/astComputeParser.d.ts +6 -0
  11. package/dist/src/ast/astComputeParser.js +1 -473
  12. package/dist/src/ast/astComputeParserHelpers.d.ts +21 -0
  13. package/dist/src/ast/astComputeParserHelpers.js +1 -0
  14. package/dist/src/ast/astDatabaseParser.d.ts +9 -24
  15. package/dist/src/ast/astDatabaseParser.js +1 -275
  16. package/dist/src/ast/astDomainParser.d.ts +139 -0
  17. package/dist/src/ast/astDomainParser.js +1 -0
  18. package/dist/src/ast/astDynamoDBParser.d.ts +35 -0
  19. package/dist/src/ast/astDynamoDBParser.js +1 -0
  20. package/dist/src/ast/astExpressionEvaluator.d.ts +23 -0
  21. package/dist/src/ast/astExpressionEvaluator.js +1 -0
  22. package/dist/src/ast/astInfrastructureParser.d.ts +12 -49
  23. package/dist/src/ast/astInfrastructureParser.js +1 -552
  24. package/dist/src/ast/astMessagingParser.d.ts +5 -0
  25. package/dist/src/ast/astMessagingParser.js +1 -78
  26. package/dist/src/ast/astNetworkParser.d.ts +6 -0
  27. package/dist/src/ast/astNetworkParser.js +1 -219
  28. package/dist/src/ast/astPatternParser.d.ts +6 -0
  29. package/dist/src/ast/astPatternParser.js +1 -155
  30. package/dist/src/ast/astPlanConverter.d.ts +11 -0
  31. package/dist/src/ast/astPlanConverter.js +2 -0
  32. package/dist/src/ast/astStatementClassifier.d.ts +24 -0
  33. package/dist/src/ast/astStatementClassifier.js +1 -0
  34. package/dist/src/ast/astStatementQueries.d.ts +21 -0
  35. package/dist/src/ast/astStatementQueries.js +3 -0
  36. package/dist/src/ast/astStorageParser.d.ts +5 -0
  37. package/dist/src/ast/astStorageParser.js +1 -164
  38. package/dist/src/ast/astSurgicalModification.js +19 -400
  39. package/dist/src/ast/astTestHelpers.d.ts +635 -0
  40. package/dist/src/ast/astTestHelpers.js +1 -0
  41. package/dist/src/ast/index.d.ts +1 -0
  42. package/dist/src/ast/index.js +1 -6
  43. package/dist/src/aws/regions.js +1 -254
  44. package/dist/src/codemod/_internal.d.ts +12 -0
  45. package/dist/src/codemod/_internal.js +1 -0
  46. package/dist/src/codemod/edits/addResource/bodyIndex.d.ts +34 -0
  47. package/dist/src/codemod/edits/addResource/bodyIndex.js +1 -0
  48. package/dist/src/codemod/edits/addResource/propertyBuilder.d.ts +7 -0
  49. package/dist/src/codemod/edits/addResource/propertyBuilder.js +1 -0
  50. package/dist/src/codemod/edits/addResource.d.ts +9 -0
  51. package/dist/src/codemod/edits/addResource.js +1 -0
  52. package/dist/src/codemod/edits/ensureImports.d.ts +26 -0
  53. package/dist/src/codemod/edits/ensureImports.js +1 -0
  54. package/dist/src/codemod/edits/findInsertionPosition.d.ts +39 -0
  55. package/dist/src/codemod/edits/findInsertionPosition.js +1 -0
  56. package/dist/src/codemod/edits/index.d.ts +5 -0
  57. package/dist/src/codemod/edits/index.js +1 -0
  58. package/dist/src/codemod/edits/modifyResource/literalConversion.d.ts +37 -0
  59. package/dist/src/codemod/edits/modifyResource/literalConversion.js +1 -0
  60. package/dist/src/codemod/edits/modifyResource.d.ts +9 -0
  61. package/dist/src/codemod/edits/modifyResource.js +1 -0
  62. package/dist/src/codemod/edits/removeResource/commentHeuristic.d.ts +31 -0
  63. package/dist/src/codemod/edits/removeResource/commentHeuristic.js +1 -0
  64. package/dist/src/codemod/edits/removeResource/importPruning.d.ts +8 -0
  65. package/dist/src/codemod/edits/removeResource/importPruning.js +1 -0
  66. package/dist/src/codemod/edits/removeResource.d.ts +10 -0
  67. package/dist/src/codemod/edits/removeResource.js +1 -0
  68. package/dist/src/codemod/fileRewriter/builders.d.ts +57 -0
  69. package/dist/src/codemod/fileRewriter/builders.js +1 -0
  70. package/dist/src/codemod/fileRewriter/index.d.ts +4 -0
  71. package/dist/src/codemod/fileRewriter/index.js +1 -0
  72. package/dist/src/codemod/fileRewriter/locateByRange.d.ts +65 -0
  73. package/dist/src/codemod/fileRewriter/locateByRange.js +1 -0
  74. package/dist/src/codemod/fileRewriter/parse.d.ts +18 -0
  75. package/dist/src/codemod/fileRewriter/parse.js +2 -0
  76. package/dist/src/codemod/fileRewriter/print.d.ts +46 -0
  77. package/dist/src/codemod/fileRewriter/print.js +4 -0
  78. package/dist/src/codemod/historyPaths.d.ts +2 -0
  79. package/dist/src/codemod/historyPaths.js +1 -0
  80. package/dist/src/codemod/index.d.ts +7 -0
  81. package/dist/src/codemod/index.js +1 -0
  82. package/dist/src/codemod/listResources.d.ts +4 -0
  83. package/dist/src/codemod/listResources.js +1 -0
  84. package/dist/src/codemod/registry.d.ts +42 -0
  85. package/dist/src/codemod/registry.js +1 -0
  86. package/dist/src/codemod/semanticIndex/findReferences.d.ts +15 -0
  87. package/dist/src/codemod/semanticIndex/findReferences.js +2 -0
  88. package/dist/src/codemod/semanticIndex/index.d.ts +4 -0
  89. package/dist/src/codemod/semanticIndex/index.js +1 -0
  90. package/dist/src/codemod/semanticIndex/listImports.d.ts +24 -0
  91. package/dist/src/codemod/semanticIndex/listImports.js +1 -0
  92. package/dist/src/codemod/semanticIndex/locateByShape.d.ts +28 -0
  93. package/dist/src/codemod/semanticIndex/locateByShape.js +1 -0
  94. package/dist/src/codemod/semanticIndex/projectCache.d.ts +14 -0
  95. package/dist/src/codemod/semanticIndex/projectCache.js +1 -0
  96. package/dist/src/codemod/types.d.ts +172 -0
  97. package/dist/src/codemod/types.js +1 -0
  98. package/dist/src/dns/bindParser.js +2 -224
  99. package/dist/src/dns/bindWriter.js +3 -52
  100. package/dist/src/dns/domainFileGenerator.d.ts +20 -0
  101. package/dist/src/dns/domainFileGenerator.js +207 -0
  102. package/dist/src/dns/domainRecords.d.ts +164 -0
  103. package/dist/src/dns/domainRecords.js +1 -0
  104. package/dist/src/dns/index.d.ts +2 -1
  105. package/dist/src/dns/index.js +1 -4
  106. package/dist/src/dns/types.js +1 -52
  107. package/dist/src/generation/common.js +6 -161
  108. package/dist/src/generation/compute.js +82 -590
  109. package/dist/src/generation/database.js +12 -198
  110. package/dist/src/generation/generatePatternCode.d.ts +58 -0
  111. package/dist/src/generation/generatePatternCode.js +33 -0
  112. package/dist/src/generation/index.js +1 -20
  113. package/dist/src/generation/infrastructure.d.ts +1 -5
  114. package/dist/src/generation/infrastructure.js +35 -377
  115. package/dist/src/generation/messagingConnections.js +1 -73
  116. package/dist/src/generation/storage.d.ts +0 -15
  117. package/dist/src/generation/storage.js +35 -168
  118. package/dist/src/generation/storageConnections.js +1 -75
  119. package/dist/src/planning/generateResourceChange.d.ts +21 -0
  120. package/dist/src/planning/generateResourceChange.js +1 -0
  121. package/dist/src/planning/index.d.ts +3 -0
  122. package/dist/src/planning/index.js +1 -1
  123. package/dist/src/planning/resourceAddition.d.ts +154 -0
  124. package/dist/src/planning/resourceAddition.js +1 -0
  125. package/dist/src/planning/resourceConnections.d.ts +19 -0
  126. package/dist/src/planning/resourceConnections.js +1 -0
  127. package/dist/src/planning/resourcePlanning.js +1 -214
  128. package/dist/src/presets/index.js +1 -3
  129. package/dist/src/presets/patternTierPresets.js +1 -131
  130. package/dist/src/presets/storagePresets.js +1 -36
  131. package/dist/src/presets/tierPresets.d.ts +5 -8
  132. package/dist/src/presets/tierPresets.js +1 -384
  133. package/dist/src/presets/tierTypes.d.ts +1 -1
  134. package/dist/src/presets/tierTypes.js +0 -7
  135. package/dist/src/schemas/alarmSchemas.d.ts +19 -0
  136. package/dist/src/schemas/alarmSchemas.js +1 -0
  137. package/dist/src/schemas/applicationSchemas.d.ts +22 -6
  138. package/dist/src/schemas/applicationSchemas.js +1 -80
  139. package/dist/src/schemas/baseSchemas.d.ts +8 -3
  140. package/dist/src/schemas/baseSchemas.js +2 -248
  141. package/dist/src/schemas/cdnSchemas.js +1 -62
  142. package/dist/src/schemas/computeSchemas.d.ts +25 -3
  143. package/dist/src/schemas/computeSchemas.js +1 -727
  144. package/dist/src/schemas/constants.d.ts +5 -7
  145. package/dist/src/schemas/constants.js +1 -218
  146. package/dist/src/schemas/databaseSchemas.d.ts +6 -1
  147. package/dist/src/schemas/databaseSchemas.js +1 -366
  148. package/dist/src/schemas/index.js +1 -3
  149. package/dist/src/schemas/instanceTypeArchitecture.js +1 -75
  150. package/dist/src/schemas/messagingSchemas.js +1 -29
  151. package/dist/src/schemas/networkSchemas.js +1 -125
  152. package/dist/src/schemas/patternSchemas.d.ts +1 -1
  153. package/dist/src/schemas/patternSchemas.js +1 -294
  154. package/dist/src/schemas/resourceSchemas.d.ts +1 -0
  155. package/dist/src/schemas/resourceSchemas.js +1 -28
  156. package/dist/src/schemas/sharedTypes.d.ts +18 -0
  157. package/dist/src/schemas/sharedTypes.js +1 -0
  158. package/dist/src/schemas/storageSchemas.d.ts +1 -0
  159. package/dist/src/schemas/storageSchemas.js +1 -119
  160. package/dist/src/types/Result.js +1 -31
  161. package/dist/src/util/errorUtils.js +1 -1
  162. package/dist/src/validation/patterns.d.ts +9 -0
  163. package/dist/src/validation/patterns.js +1 -369
  164. package/dist/src/version.d.ts +1 -1
  165. package/dist/src/version.js +1 -1
  166. package/package.json +29 -9
  167. package/dist/src/dns/infrastructureWriter.d.ts +0 -2
  168. package/dist/src/dns/infrastructureWriter.js +0 -58
@@ -1,164 +1 @@
1
- import * as ts from "typescript";
2
- import { S3_STACK_PLACEMENTS, } from "../schemas/resourceSchemas.js";
3
- import { S3_ENCRYPTION_TYPES, BACKUP_VAULT_TIERS, } from "../schemas/constants.js";
4
- import { asBoolean, asNumber, asString, asStringUnion, captureExtraProperties, collectFromAst, extractVariableName, isFactoryBuildCall, isFactoryMethodCall, isParsedObject, parseObjectLiteral, } from "./astCommonParser.js";
5
- const S3_BUCKET_CLASSES = new Set(["S3Bucket"]);
6
- export { S3_BUCKET_CLASSES };
7
- // ---- Extraction helpers ----
8
- function extractS3Resource(varDecl, newExpr, bucketClass) {
9
- const variableName = ts.isIdentifier(varDecl.name) ? varDecl.name.text : "";
10
- if (newExpr.arguments && newExpr.arguments.length >= 2) {
11
- const nameArg = newExpr.arguments[1];
12
- const resourceName = ts.isStringLiteral(nameArg) ? nameArg.text : "";
13
- const configArg = newExpr.arguments.length >= 3 ? newExpr.arguments[2] : undefined;
14
- const config = configArg && ts.isObjectLiteralExpression(configArg)
15
- ? parseObjectLiteral(configArg)
16
- : {};
17
- return {
18
- variableName,
19
- resourceName,
20
- bucketClass,
21
- config,
22
- node: varDecl,
23
- };
24
- }
25
- return null;
26
- }
27
- function extractS3FactoryResource(addStorageCall, buildCall) {
28
- if (buildCall.arguments.length < 2)
29
- return null;
30
- const nameArg = buildCall.arguments[0];
31
- const configArg = buildCall.arguments[1];
32
- if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(configArg))
33
- return null;
34
- const config = parseObjectLiteral(configArg);
35
- const variableName = extractVariableName(addStorageCall) ?? "";
36
- return {
37
- variableName,
38
- resourceName: nameArg.text,
39
- bucketClass: "StorageFactory",
40
- config,
41
- node: addStorageCall,
42
- };
43
- }
44
- // ---- Public API ----
45
- /** Find S3 resources created via new S3Bucket() constructor pattern */
46
- export function findS3Resources(sourceFile) {
47
- return collectFromAst(sourceFile, (node) => {
48
- if (!ts.isVariableDeclaration(node) || !node.initializer)
49
- return null;
50
- if (!ts.isNewExpression(node.initializer))
51
- return null;
52
- const newExpr = node.initializer;
53
- if (!ts.isIdentifier(newExpr.expression))
54
- return null;
55
- const bucketClass = newExpr.expression.text;
56
- if (!S3_BUCKET_CLASSES.has(bucketClass))
57
- return null;
58
- return extractS3Resource(node, newExpr, bucketClass);
59
- });
60
- }
61
- /** Find S3 resources created via app.addStorage(StorageFactory.build(...)) factory pattern */
62
- export function findS3FactoryResources(sourceFile) {
63
- return collectFromAst(sourceFile, (node) => {
64
- if (!ts.isCallExpression(node) || !isFactoryMethodCall(node, "addStorage"))
65
- return null;
66
- const storageArg = node.arguments[0];
67
- if (!isFactoryBuildCall(storageArg, "StorageFactory"))
68
- return null;
69
- return extractS3FactoryResource(node, storageArg);
70
- });
71
- }
72
- /** Convert parsed S3 resources to plan format */
73
- export function convertS3Resources(s3Resources) {
74
- return s3Resources.map((s3) => ({
75
- name: s3.resourceName,
76
- ...(asString(s3.config.bucketName) && {
77
- bucketName: asString(s3.config.bucketName),
78
- }),
79
- ...(asBoolean(s3.config.publicReadAccess) === true && {
80
- publicReadAccess: true,
81
- }),
82
- ...(isParsedObject(s3.config.websiteHosting) &&
83
- (() => {
84
- const hosting = s3.config.websiteHosting;
85
- return {
86
- websiteHosting: {
87
- indexDocument: asString(hosting.indexDocument) ?? "index.html",
88
- ...(asString(hosting.errorDocument) && {
89
- errorDocument: asString(hosting.errorDocument),
90
- }),
91
- },
92
- };
93
- })()),
94
- ...(asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS) && {
95
- backupVaultTier: asStringUnion(s3.config.backupVaultTier, BACKUP_VAULT_TIERS),
96
- }),
97
- ...(asBoolean(s3.config.versioned) !== undefined && {
98
- versioned: asBoolean(s3.config.versioned),
99
- }),
100
- ...(asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES) && {
101
- encryption: asStringUnion(s3.config.encryption, S3_ENCRYPTION_TYPES),
102
- }),
103
- ...(asString(s3.config.kmsKeyArn) && {
104
- kmsKeyArn: asString(s3.config.kmsKeyArn),
105
- }),
106
- ...(Array.isArray(s3.config.cors) &&
107
- s3.config.cors.length > 0 && {
108
- cors: s3.config.cors.filter(isParsedObject).map((rule) => ({
109
- allowedOrigins: Array.isArray(rule.allowedOrigins)
110
- ? rule.allowedOrigins.filter((o) => typeof o === "string")
111
- : [],
112
- allowedMethods: Array.isArray(rule.allowedMethods)
113
- ? rule.allowedMethods.filter((m) => typeof m === "string")
114
- : [],
115
- })),
116
- }),
117
- ...(isParsedObject(s3.config.deployment) &&
118
- (() => {
119
- const deployment = s3.config.deployment;
120
- return {
121
- deployment: {
122
- source: asString(deployment.source) ?? "",
123
- ...(asBoolean(deployment.prune) !== undefined && {
124
- prune: asBoolean(deployment.prune),
125
- }),
126
- ...(isParsedObject(deployment.cacheControl) &&
127
- (() => {
128
- const cache = deployment.cacheControl;
129
- return {
130
- cacheControl: {
131
- ...(asNumber(cache.maxAge) !== undefined && {
132
- maxAge: asNumber(cache.maxAge),
133
- }),
134
- ...(asBoolean(cache.immutable) !== undefined && {
135
- immutable: asBoolean(cache.immutable),
136
- }),
137
- },
138
- };
139
- })()),
140
- },
141
- };
142
- })()),
143
- ...(asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS) && {
144
- stackPlacement: asStringUnion(s3.config.stackPlacement, S3_STACK_PLACEMENTS),
145
- }),
146
- ...(s3.variableName && { variableName: s3.variableName }),
147
- ...(() => {
148
- const S3_KNOWN_KEYS = new Set([
149
- "bucketName",
150
- "publicReadAccess",
151
- "websiteHosting",
152
- "backupVaultTier",
153
- "versioned",
154
- "encryption",
155
- "kmsKeyArn",
156
- "cors",
157
- "deployment",
158
- "stackPlacement",
159
- ]);
160
- const extras = captureExtraProperties(s3.config, S3_KNOWN_KEYS);
161
- return extras.length > 0 ? { extraProperties: extras } : {};
162
- })(),
163
- }));
164
- }
1
+ import*as i from"typescript";import{S3_STACK_PLACEMENTS as g}from"../schemas/resourceSchemas.js";import{S3_ENCRYPTION_TYPES as f,BACKUP_VAULT_TIERS as p}from"../schemas/constants.js";import{asBoolean as c,asNumber as d,asString as o,asStringUnion as a,captureExtraProperties as x,collectFromAst as b,extractVariableName as N,isFactoryBuildCall as k,isFactoryMethodCall as h,isParsedObject as u,parseObjectLiteral as y}from"./astCommonParser.js";const A=new Set(["S3Bucket"]);function C(n,e,r){const t=i.isIdentifier(n.name)?n.name.text:"";if(e.arguments&&e.arguments.length>=2){const s=e.arguments[1],m=i.isStringLiteral(s)?s.text:"",l=e.arguments.length>=3?e.arguments[2]:void 0,S=l&&i.isObjectLiteralExpression(l)?y(l):{};return{variableName:t,resourceName:m,bucketClass:r,config:S,node:n}}return null}function P(n,e){if(e.arguments.length<2)return null;const r=e.arguments[0],t=e.arguments[1];if(!i.isStringLiteral(r)||!i.isObjectLiteralExpression(t))return null;const s=y(t);return{variableName:N(n)??"",resourceName:r.text,bucketClass:"StorageFactory",config:s,node:n}}function R(n){return b(n,e=>{if(!i.isVariableDeclaration(e)||!e.initializer||!i.isNewExpression(e.initializer))return null;const r=e.initializer;if(!i.isIdentifier(r.expression))return null;const t=r.expression.text;return A.has(t)?C(e,r,t):null})}function _(n){return b(n,e=>{if(!i.isCallExpression(e)||!h(e,"addStorage"))return null;const r=e.arguments[0];return k(r,"StorageFactory")?P(e,r):null})}function v(n){return n.map(e=>({name:e.resourceName,...o(e.config.bucketName)&&{bucketName:o(e.config.bucketName)},...c(e.config.publicReadAccess)===!0&&{publicReadAccess:!0},...u(e.config.websiteHosting)&&(()=>{const r=e.config.websiteHosting;return{websiteHosting:{indexDocument:o(r.indexDocument)??"index.html",...o(r.errorDocument)&&{errorDocument:o(r.errorDocument)}}}})(),...a(e.config.backupVaultTier,p)&&{backupVaultTier:a(e.config.backupVaultTier,p)},...c(e.config.versioned)!==void 0&&{versioned:c(e.config.versioned)},...a(e.config.encryption,f)&&{encryption:a(e.config.encryption,f)},...o(e.config.kmsKeyArn)&&{kmsKeyArn:o(e.config.kmsKeyArn)},...Array.isArray(e.config.cors)&&e.config.cors.length>0&&{cors:e.config.cors.filter(u).map(r=>({allowedOrigins:Array.isArray(r.allowedOrigins)?r.allowedOrigins.filter(t=>typeof t=="string"):[],allowedMethods:Array.isArray(r.allowedMethods)?r.allowedMethods.filter(t=>typeof t=="string"):[]}))},...u(e.config.deployment)&&(()=>{const r=e.config.deployment;return{deployment:{source:o(r.source)??"",...c(r.prune)!==void 0&&{prune:c(r.prune)},...u(r.cacheControl)&&(()=>{const t=r.cacheControl;return{cacheControl:{...d(t.maxAge)!==void 0&&{maxAge:d(t.maxAge)},...c(t.immutable)!==void 0&&{immutable:c(t.immutable)}}}})()}}})(),...a(e.config.stackPlacement,g)&&{stackPlacement:a(e.config.stackPlacement,g)},...e.variableName&&{variableName:e.variableName},...(()=>{const r=new Set(["bucketName","publicReadAccess","websiteHosting","backupVaultTier","versioned","encryption","kmsKeyArn","cors","deployment","stackPlacement"]),t=x(e.config,r);return t.length>0?{extraProperties:t}:{}})()}))}export{A as S3_BUCKET_CLASSES,v as convertS3Resources,_ as findS3FactoryResources,R as findS3Resources};
@@ -1,400 +1,19 @@
1
- import * as ts from "typescript";
2
- import { parseInfrastructure, classifyStatements, findManagedResourcePosition, } from "./astInfrastructureParser.js";
3
- import { getErrorMessage } from "../util/errorUtils.js";
4
- function insertAtPosition(content, position, text) {
5
- return content.slice(0, position) + text + content.slice(position);
6
- }
7
- function findClassification(classifications, type) {
8
- return classifications.find((c) => c.type === type);
9
- }
10
- function findLastClassification(classifications, type) {
11
- return classifications.filter((c) => c.type === type).pop();
12
- }
13
- function parseSourceFile(content) {
14
- return ts.createSourceFile("infrastructure.ts", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
15
- }
16
- function formatLeadingNewlines(beforeText, codeToInsert) {
17
- if (beforeText.endsWith("\n\n") || codeToInsert.startsWith("\n")) {
18
- return "";
19
- }
20
- if (!beforeText.endsWith("\n")) {
21
- return "\n\n";
22
- }
23
- return "\n";
24
- }
25
- function getContextBeforePosition(content, position) {
26
- return content.slice(Math.max(0, position - 2), position);
27
- }
28
- // getErrorMessage is now imported from errorUtils.ts
29
- function formatResourceId(type, name) {
30
- return name ? `${type}:${name}` : type;
31
- }
32
- function createErrorResult(error, content) {
33
- return { content, success: false, error: getErrorMessage(error) };
34
- }
35
- function findResourceOrFail(sourceFile, content, resourceType, resourceName) {
36
- const resourcePos = findManagedResourcePosition(sourceFile, resourceType, resourceName);
37
- if (!resourcePos) {
38
- return createErrorResult(new Error(`Resource not found: ${formatResourceId(resourceType, resourceName)}`), content);
39
- }
40
- return resourcePos;
41
- }
42
- function isFailureResult(result) {
43
- return "success" in result && !result.success;
44
- }
45
- export function addResourceSurgically(content, options) {
46
- const { resourceType, code, afterResource } = options;
47
- try {
48
- const sourceFile = parseSourceFile(content);
49
- let insertPosition;
50
- if (afterResource) {
51
- const afterPos = findManagedResourcePosition(sourceFile, afterResource.type, afterResource.name);
52
- if (!afterPos) {
53
- return createErrorResult(new Error(`Insertion target not found: ${formatResourceId(afterResource.type, afterResource.name)}`), content);
54
- }
55
- insertPosition = afterPos.endPos;
56
- }
57
- else {
58
- insertPosition = findInsertionPositionByType(sourceFile, resourceType);
59
- }
60
- const codeToInsert = formatCodeForInsertion(content, insertPosition, code);
61
- const newContent = insertAtPosition(content, insertPosition, codeToInsert);
62
- return {
63
- content: newContent,
64
- success: true,
65
- };
66
- }
67
- catch (error) {
68
- return createErrorResult(error, content);
69
- }
70
- }
71
- /**
72
- * Find the best insertion position for a resource type.
73
- * Resources are ordered: imports > app-init > tags > database > storage > compute > network > pattern
74
- */
75
- function findInsertionPositionByType(sourceFile, resourceType) {
76
- const classifications = classifyStatements(sourceFile);
77
- // Define ordering for resource types
78
- const typeOrder = [
79
- "import",
80
- "app-init",
81
- "tags",
82
- "database",
83
- "storage",
84
- "messaging",
85
- "compute",
86
- "network",
87
- "cdn",
88
- "pattern",
89
- ];
90
- const targetIndex = typeOrder.indexOf(resourceType);
91
- // Find the last statement of the same type or earlier types
92
- let lastRelevantStatement = null;
93
- for (const classification of classifications) {
94
- if (classification.isManaged) {
95
- const classIndex = typeOrder.indexOf(classification.type);
96
- if (classIndex !== -1 && classIndex <= targetIndex) {
97
- lastRelevantStatement = classification;
98
- }
99
- }
100
- }
101
- if (lastRelevantStatement) {
102
- return lastRelevantStatement.endPos;
103
- }
104
- // Fall back to end of file
105
- return sourceFile.getEnd();
106
- }
107
- function formatCodeForInsertion(content, position, code) {
108
- const beforeText = getContextBeforePosition(content, position);
109
- const afterText = content.slice(position, Math.min(content.length, position + 2));
110
- const leadingNewlines = formatLeadingNewlines(beforeText, code);
111
- const trailingNewlines = afterText.startsWith("\n") ? "" : "\n";
112
- return leadingNewlines + code + trailingNewlines;
113
- }
114
- export function updateResourceSurgically(content, options) {
115
- const { resourceType, resourceName, newCode } = options;
116
- try {
117
- const sourceFile = parseSourceFile(content);
118
- const result = findResourceOrFail(sourceFile, content, resourceType, resourceName);
119
- if (isFailureResult(result))
120
- return result;
121
- const resourcePos = result;
122
- // getFullStart() includes leading comments/whitespace
123
- const fullStart = resourcePos.node.getFullStart();
124
- const end = resourcePos.endPos;
125
- // Preserve any leading whitespace from the original
126
- const originalLeading = content.slice(fullStart, resourcePos.node.getStart());
127
- const leadingWhitespace = extractLeadingWhitespace(originalLeading);
128
- const newContent = content.slice(0, fullStart) +
129
- leadingWhitespace +
130
- newCode +
131
- content.slice(end);
132
- return {
133
- content: newContent,
134
- success: true,
135
- };
136
- }
137
- catch (error) {
138
- return createErrorResult(error, content);
139
- }
140
- }
141
- function extractLeadingWhitespace(text) {
142
- const match = text.match(/^[\s]*[\n\r]+[\s]*/);
143
- return match ? match[0] : "";
144
- }
145
- // Track search offset to avoid matching the same location twice when
146
- // similar text appears in multiple places.
147
- function insertOrphanMarkers(content, blocks, resourceType, resourceName) {
148
- let result = content;
149
- let searchFrom = 0;
150
- for (const block of blocks) {
151
- const orphanComment = `// [ORPHANED: was after ${formatResourceId(resourceType, resourceName)}]\n`;
152
- const trimmedText = block.sourceText.trim();
153
- const blockPos = result.indexOf(trimmedText, searchFrom);
154
- if (blockPos !== -1) {
155
- result = insertAtPosition(result, blockPos, orphanComment);
156
- searchFrom = blockPos + orphanComment.length + trimmedText.length;
157
- }
158
- }
159
- return result;
160
- }
161
- export function removeResourceSurgically(content, options) {
162
- const { resourceType, resourceName, orphanHandling } = options;
163
- const warnings = [];
164
- try {
165
- const sourceFile = parseSourceFile(content);
166
- const result = findResourceOrFail(sourceFile, content, resourceType, resourceName);
167
- if (isFailureResult(result))
168
- return result;
169
- const resourcePos = result;
170
- // Check for custom code blocks that reference this resource
171
- const parsed = parseInfrastructure(content, { extractCustomCode: true });
172
- const affectedCustomBlocks = (parsed.customCodeBlocks ?? []).filter((block) => block.position === "after-resource" &&
173
- block.afterManagedResource?.type === resourceType &&
174
- block.afterManagedResource?.name === resourceName);
175
- if (affectedCustomBlocks.length > 0) {
176
- if (orphanHandling === "preserve-with-warning") {
177
- warnings.push(`Custom code after ${formatResourceId(resourceType, resourceName)} will be orphaned. ` +
178
- `Added // [ORPHANED] marker comment.`);
179
- }
180
- }
181
- // Get the removal range including leading whitespace
182
- const fullStart = resourcePos.node.getFullStart();
183
- const end = resourcePos.endPos;
184
- // Check if there's a trailing newline to clean up
185
- const removeEnd = content[end] === "\n" ? end + 1 : end;
186
- let newContent = content.slice(0, fullStart) + content.slice(removeEnd);
187
- if (affectedCustomBlocks.length > 0 &&
188
- orphanHandling === "preserve-with-warning") {
189
- newContent = insertOrphanMarkers(newContent, affectedCustomBlocks, resourceType, resourceName);
190
- }
191
- return {
192
- content: newContent,
193
- success: true,
194
- warnings: warnings.length > 0 ? warnings : undefined,
195
- };
196
- }
197
- catch (error) {
198
- return createErrorResult(error, content);
199
- }
200
- }
201
- export function ensureImports(content, requiredImports) {
202
- try {
203
- const sourceFile = parseSourceFile(content);
204
- const existingImports = new Set();
205
- for (const statement of sourceFile.statements) {
206
- if (ts.isImportDeclaration(statement)) {
207
- existingImports.add(statement.getText(sourceFile).trim());
208
- }
209
- }
210
- const importsToAdd = [];
211
- for (const importStmt of requiredImports) {
212
- const normalisedImport = importStmt.trim();
213
- const exists = [...existingImports].some((existing) => importsAreEquivalent(existing, normalisedImport));
214
- if (!exists) {
215
- importsToAdd.push(normalisedImport);
216
- }
217
- }
218
- if (importsToAdd.length === 0) {
219
- return {
220
- content,
221
- success: true,
222
- };
223
- }
224
- const lastImportEnd = sourceFile.statements.filter(ts.isImportDeclaration).at(-1)?.getEnd() ??
225
- 0;
226
- const importText = "\n" + importsToAdd.join("\n");
227
- const newContent = insertAtPosition(content, lastImportEnd, importText);
228
- return {
229
- content: newContent,
230
- success: true,
231
- };
232
- }
233
- catch (error) {
234
- return createErrorResult(error, content);
235
- }
236
- }
237
- function parseNamedImports(raw) {
238
- return new Set(raw
239
- .split(",")
240
- .map((s) => s.trim())
241
- .filter(Boolean));
242
- }
243
- /** Existing import (import1) is a superset of or equal to the required one (import2). */
244
- function existingImportCoversRequired(existingRaw, requiredRaw) {
245
- const existing = parseNamedImports(existingRaw);
246
- const required = parseNamedImports(requiredRaw);
247
- for (const name of required) {
248
- if (!existing.has(name))
249
- return false;
250
- }
251
- return true;
252
- }
253
- /**
254
- * Check if two import statements are equivalent (same module, overlapping named imports).
255
- */
256
- function importsAreEquivalent(import1, import2) {
257
- const moduleMatch1 = import1.match(FROM_MODULE_REGEX);
258
- const moduleMatch2 = import2.match(FROM_MODULE_REGEX);
259
- if (!moduleMatch1 || !moduleMatch2) {
260
- return false;
261
- }
262
- if (moduleMatch1[1] !== moduleMatch2[1]) {
263
- return false;
264
- }
265
- // Same module - compare named import bindings when both use them
266
- const namedMatch1 = import1.match(NAMED_IMPORTS_REGEX);
267
- const namedMatch2 = import2.match(NAMED_IMPORTS_REGEX);
268
- if (namedMatch1 && namedMatch2) {
269
- return existingImportCoversRequired(namedMatch1[1], namedMatch2[1]);
270
- }
271
- return true;
272
- }
273
- const FROM_MODULE_REGEX = /from\s+["']([^"']+)["']/;
274
- const NAMED_IMPORTS_REGEX = /\{([^}]+)\}/;
275
- const POSITION_ORDER = {
276
- "before-imports": 0,
277
- "after-imports": 1,
278
- "after-app-init": 2,
279
- "after-tags": 3,
280
- "after-resource": 4,
281
- "end-of-file": 5,
282
- };
283
- function sortCustomBlocksForInsertion(blocks) {
284
- return [...blocks].sort((a, b) => {
285
- const positionDiff = POSITION_ORDER[b.position] - POSITION_ORDER[a.position];
286
- if (positionDiff !== 0)
287
- return positionDiff;
288
- return (b.originalLine ?? 0) - (a.originalLine ?? 0);
289
- });
290
- }
291
- function insertOrOrphanBlock(content, block, insertPos, warnings) {
292
- if (insertPos !== null) {
293
- const codeToInsert = formatCustomCodeForInsertion(content, insertPos, block);
294
- return insertAtPosition(content, insertPos, codeToInsert);
295
- }
296
- const orphanComment = block.afterManagedResource
297
- ? `// [ORPHANED: was after ${formatResourceId(block.afterManagedResource.type, block.afterManagedResource.name)}]\n`
298
- : "// [ORPHANED]\n";
299
- warnings.push(`Custom code from line ${block.originalLine} could not be positioned. Added at end of file.`);
300
- return content + "\n" + orphanComment + block.sourceText;
301
- }
302
- export function injectCustomCodeBlocks(generatedCode, customBlocks, resourceMapping) {
303
- if (!customBlocks || customBlocks.length === 0) {
304
- return { content: generatedCode, success: true };
305
- }
306
- try {
307
- const sourceFile = parseSourceFile(generatedCode);
308
- const classifications = classifyStatements(sourceFile);
309
- const warnings = [];
310
- const processedBlocks = sortCustomBlocksForInsertion(customBlocks);
311
- let result = generatedCode;
312
- for (const block of processedBlocks) {
313
- const insertPos = findCustomCodeInsertPosition(sourceFile, classifications, block, resourceMapping, warnings);
314
- result = insertOrOrphanBlock(result, block, insertPos, warnings);
315
- }
316
- return {
317
- content: result,
318
- success: true,
319
- warnings: warnings.length > 0 ? warnings : undefined,
320
- };
321
- }
322
- catch (error) {
323
- return createErrorResult(error, generatedCode);
324
- }
325
- }
326
- function findAfterResourcePosition(classifications, block, resourceMapping, warnings) {
327
- if (!block.afterManagedResource)
328
- return null;
329
- const afterResource = block.afterManagedResource;
330
- const targetName = (afterResource.name && resourceMapping?.get(afterResource.name)) ??
331
- afterResource.name;
332
- const resource = classifications.find((c) => c.type === afterResource.type && c.resourceName === targetName);
333
- if (resource)
334
- return resource.endPos;
335
- // Fallback: if exactly one resource of this type, use it
336
- const typeMatches = classifications.filter((c) => c.type === afterResource.type && c.isManaged);
337
- if (typeMatches.length === 1)
338
- return typeMatches[0].endPos;
339
- if (warnings) {
340
- warnings.push(`Resource ${formatResourceId(block.afterManagedResource.type, block.afterManagedResource.name)} not found. ` +
341
- `Custom code may be orphaned.`);
342
- }
343
- return null;
344
- }
345
- function findCustomCodeInsertPosition(sourceFile, classifications, block, resourceMapping, warnings) {
346
- switch (block.position) {
347
- case "before-imports":
348
- return 0;
349
- case "after-imports":
350
- return findLastClassification(classifications, "import")?.endPos ?? 0;
351
- case "after-app-init":
352
- return findClassification(classifications, "app-init")?.endPos ?? null;
353
- case "after-tags":
354
- return findClassification(classifications, "tags")?.endPos ?? null;
355
- case "after-resource":
356
- return findAfterResourcePosition(classifications, block, resourceMapping, warnings);
357
- case "end-of-file":
358
- return sourceFile.getEnd();
359
- default:
360
- return null;
361
- }
362
- }
363
- function formatCustomCodeForInsertion(content, position, block) {
364
- const beforeText = getContextBeforePosition(content, position);
365
- let code = block.sourceText;
366
- const leadingNewlines = formatLeadingNewlines(beforeText, code);
367
- code = leadingNewlines + code;
368
- if (!code.endsWith("\n")) {
369
- code += "\n";
370
- }
371
- return code;
372
- }
373
- export function validateModifiedFile(content) {
374
- try {
375
- const sourceFile = parseSourceFile(content);
376
- // ts.createSourceFile doesn't fully validate, but it catches major syntax errors
377
- const diagnostics = [];
378
- // Walk the tree looking for problematic nodes
379
- function visit(node) {
380
- if (node.kind === ts.SyntaxKind.Unknown) {
381
- diagnostics.push("Unknown node found in AST - possible parse error");
382
- }
383
- ts.forEachChild(node, visit);
384
- }
385
- visit(sourceFile);
386
- if (diagnostics.length > 0) {
387
- return {
388
- valid: false,
389
- errors: diagnostics,
390
- };
391
- }
392
- return { valid: true };
393
- }
394
- catch (error) {
395
- return {
396
- valid: false,
397
- errors: [getErrorMessage(error)],
398
- };
399
- }
400
- }
1
+ import*as f from"typescript";import{parseInfrastructure as D,classifyStatements as y,findManagedResourcePosition as C}from"./astInfrastructureParser.js";import{getErrorMessage as M}from"../util/errorUtils.js";function R(e,t,r){return e.slice(0,t)+r+e.slice(t)}function P(e,t){return e.find(r=>r.type===t)}function L(e,t){return e.filter(r=>r.type===t).pop()}function m(e){return f.createSourceFile("infrastructure.ts",e,f.ScriptTarget.Latest,!0,f.ScriptKind.TS)}function I(e,t){return e.endsWith(`
2
+
3
+ `)||t.startsWith(`
4
+ `)?"":e.endsWith(`
5
+ `)?`
6
+ `:`
7
+
8
+ `}function E(e,t){return e.slice(Math.max(0,t-2),t)}function p(e,t){return t?`${e}:${t}`:e}function l(e,t){return{content:t,success:!1,error:M(e)}}function S(e,t,r,s){const n=C(e,r,s);return n||l(new Error(`Resource not found: ${p(r,s)}`),t)}function T(e){return"success"in e&&!e.success}function J(e,t){const{resourceType:r,code:s,afterResource:n}=t;try{const o=m(e);let i;if(n){const u=C(o,n.type,n.name);if(!u)return l(new Error(`Insertion target not found: ${p(n.type,n.name)}`),e);i=u.endPos}else i=$(o,r);const c=B(e,i,s);return{content:R(e,i,c),success:!0}}catch(o){return l(o,e)}}function $(e,t){const r=y(e),s=["import","app-init","tags","database","storage","messaging","compute","network","cdn","pattern"],n=s.indexOf(t);let o=null;for(const i of r)if(i.isManaged){const c=s.indexOf(i.type);c!==-1&&c<=n&&(o=i)}return o?o.endPos:e.getEnd()}function B(e,t,r){const s=E(e,t),n=e.slice(t,Math.min(e.length,t+2)),o=I(s,r),i=n.startsWith(`
9
+ `)?"":`
10
+ `;return o+r+i}function Q(e,t){const{resourceType:r,resourceName:s,newCode:n}=t;try{const o=m(e),i=S(o,e,r,s);if(T(i))return i;const c=i,a=c.node.getFullStart(),u=c.endPos,d=e.slice(a,c.node.getStart()),g=W(d);return{content:e.slice(0,a)+g+n+e.slice(u),success:!0}}catch(o){return l(o,e)}}function W(e){const t=e.match(/^[\s]*[\n\r]+[\s]*/);return t?t[0]:""}function H(e,t,r,s){let n=e,o=0;for(const i of t){const c=`// [ORPHANED: was after ${p(r,s)}]
11
+ `,a=i.sourceText.trim(),u=n.indexOf(a,o);u!==-1&&(n=R(n,u,c),o=u+c.length+a.length)}return n}function V(e,t){const{resourceType:r,resourceName:s,orphanHandling:n}=t,o=[];try{const i=m(e),c=S(i,e,r,s);if(T(c))return c;const a=c,d=(D(e,{extractCustomCode:!0}).customCodeBlocks??[]).filter(x=>x.position==="after-resource"&&x.afterManagedResource?.type===r&&x.afterManagedResource?.name===s);d.length>0&&n==="preserve-with-warning"&&o.push(`Custom code after ${p(r,s)} will be orphaned. Added // [ORPHANED] marker comment.`);const g=a.node.getFullStart(),h=a.endPos,A=e[h]===`
12
+ `?h+1:h;let w=e.slice(0,g)+e.slice(A);return d.length>0&&n==="preserve-with-warning"&&(w=H(w,d,r,s)),{content:w,success:!0,warnings:o.length>0?o:void 0}}catch(i){return l(i,e)}}function Y(e,t){try{const r=m(e),s=new Set;for(const a of r.statements)f.isImportDeclaration(a)&&s.add(a.getText(r).trim());const n=[];for(const a of t){const u=a.trim();[...s].some(g=>q(g,u))||n.push(u)}if(n.length===0)return{content:e,success:!0};const o=r.statements.filter(f.isImportDeclaration).at(-1)?.getEnd()??0,i=`
13
+ `+n.join(`
14
+ `);return{content:R(e,o,i),success:!0}}catch(r){return l(r,e)}}function O(e){return new Set(e.split(",").map(t=>t.trim()).filter(Boolean))}function _(e,t){const r=O(e),s=O(t);for(const n of s)if(!r.has(n))return!1;return!0}function q(e,t){const r=e.match(F),s=t.match(F);if(!r||!s||r[1]!==s[1])return!1;const n=e.match(N),o=t.match(N);return n&&o?_(n[1],o[1]):!0}const F=/from\s+["']([^"']+)["']/,N=/\{([^}]+)\}/,v={"before-imports":0,"after-imports":1,"after-app-init":2,"after-tags":3,"after-resource":4,"end-of-file":5};function U(e){return[...e].sort((t,r)=>{const s=v[r.position]-v[t.position];return s!==0?s:(r.originalLine??0)-(t.originalLine??0)})}function j(e,t,r,s){if(r!==null){const o=K(e,r,t);return R(e,r,o)}const n=t.afterManagedResource?`// [ORPHANED: was after ${p(t.afterManagedResource.type,t.afterManagedResource.name)}]
15
+ `:`// [ORPHANED]
16
+ `;return s.push(`Custom code from line ${t.originalLine} could not be positioned. Added at end of file.`),e+`
17
+ `+n+t.sourceText}function Z(e,t,r){if(!t||t.length===0)return{content:e,success:!0};try{const s=m(e),n=y(s),o=[],i=U(t);let c=e;for(const a of i){const u=G(s,n,a,r,o);c=j(c,a,u,o)}return{content:c,success:!0,warnings:o.length>0?o:void 0}}catch(s){return l(s,e)}}function k(e,t,r,s){if(!t.afterManagedResource)return null;const n=t.afterManagedResource,o=(n.name&&r?.get(n.name))??n.name,i=e.find(a=>a.type===n.type&&a.resourceName===o);if(i)return i.endPos;const c=e.filter(a=>a.type===n.type&&a.isManaged);return c.length===1?c[0].endPos:(s&&s.push(`Resource ${p(t.afterManagedResource.type,t.afterManagedResource.name)} not found. Custom code may be orphaned.`),null)}function G(e,t,r,s,n){switch(r.position){case"before-imports":return 0;case"after-imports":return L(t,"import")?.endPos??0;case"after-app-init":return P(t,"app-init")?.endPos??null;case"after-tags":return P(t,"tags")?.endPos??null;case"after-resource":return k(t,r,s,n);case"end-of-file":return e.getEnd();default:return null}}function K(e,t,r){const s=E(e,t);let n=r.sourceText;return n=I(s,n)+n,n.endsWith(`
18
+ `)||(n+=`
19
+ `),n}function b(e){try{let s=function(n){n.kind===f.SyntaxKind.Unknown&&r.push("Unknown node found in AST - possible parse error"),f.forEachChild(n,s)};const t=m(e),r=[];return s(t),r.length>0?{valid:!1,errors:r}:{valid:!0}}catch(t){return{valid:!1,errors:[M(t)]}}}export{J as addResourceSurgically,Y as ensureImports,Z as injectCustomCodeBlocks,V as removeResourceSurgically,Q as updateResourceSurgically,b as validateModifiedFile};