@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.
- package/dist/src/ast/astComputeParser.d.ts +4 -0
- package/dist/src/ast/astComputeParser.js +427 -0
- package/dist/src/ast/astInfrastructureParser.d.ts +357 -0
- package/dist/src/ast/astInfrastructureParser.js +1925 -0
- package/dist/src/ast/astSurgicalModification.d.ts +47 -0
- package/dist/src/ast/astSurgicalModification.js +400 -0
- package/dist/src/ast/index.d.ts +2 -0
- package/dist/src/ast/index.js +2 -0
- package/dist/src/aws/regions.d.ts +30 -0
- package/dist/src/aws/regions.js +254 -0
- package/dist/src/generation/common.d.ts +86 -0
- package/dist/src/generation/common.js +187 -0
- package/dist/src/generation/compute.d.ts +6 -0
- package/dist/src/generation/compute.js +547 -0
- package/dist/src/generation/database.d.ts +54 -0
- package/dist/src/generation/database.js +201 -0
- package/dist/src/generation/index.d.ts +12 -0
- package/dist/src/generation/index.js +18 -0
- package/dist/src/generation/infrastructure.d.ts +44 -0
- package/dist/src/generation/infrastructure.js +389 -0
- package/dist/src/generation/storage.d.ts +23 -0
- package/dist/src/generation/storage.js +174 -0
- package/dist/src/generation/storageConnections.d.ts +37 -0
- package/dist/src/generation/storageConnections.js +71 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +19 -0
- package/dist/src/planning/index.d.ts +1 -0
- package/dist/src/planning/index.js +1 -0
- package/dist/src/planning/resourcePlanning.d.ts +58 -0
- package/dist/src/planning/resourcePlanning.js +216 -0
- package/dist/src/presets/index.d.ts +3 -0
- package/dist/src/presets/index.js +3 -0
- package/dist/src/presets/patternTierPresets.d.ts +93 -0
- package/dist/src/presets/patternTierPresets.js +131 -0
- package/dist/src/presets/storagePresets.d.ts +11 -0
- package/dist/src/presets/storagePresets.js +36 -0
- package/dist/src/presets/tierPresets.d.ts +59 -0
- package/dist/src/presets/tierPresets.js +384 -0
- package/dist/src/presets/tierTypes.d.ts +301 -0
- package/dist/src/presets/tierTypes.js +7 -0
- package/dist/src/schemas/constants.d.ts +74 -0
- package/dist/src/schemas/constants.js +208 -0
- package/dist/src/schemas/index.d.ts +3 -0
- package/dist/src/schemas/index.js +3 -0
- package/dist/src/schemas/instanceTypeArchitecture.d.ts +35 -0
- package/dist/src/schemas/instanceTypeArchitecture.js +75 -0
- package/dist/src/schemas/resourceSchemas.d.ts +3534 -0
- package/dist/src/schemas/resourceSchemas.js +2015 -0
- package/dist/src/types/Result.d.ts +19 -0
- package/dist/src/types/Result.js +31 -0
- package/dist/src/util/errorUtils.d.ts +2 -0
- package/dist/src/util/errorUtils.js +15 -0
- package/dist/src/validation/patterns.d.ts +300 -0
- package/dist/src/validation/patterns.js +360 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type StatementType, type CustomCodeBlock } from "./astInfrastructureParser.js";
|
|
2
|
+
export interface SurgicalModificationResult {
|
|
3
|
+
content: string;
|
|
4
|
+
success: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
/** e.g., orphaned custom code */
|
|
7
|
+
warnings?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface InsertionOptions {
|
|
10
|
+
/** The type of resource being inserted */
|
|
11
|
+
resourceType: StatementType;
|
|
12
|
+
/** The resource name (e.g., "MyDatabase") */
|
|
13
|
+
resourceName: string;
|
|
14
|
+
/** The code to insert (complete statement) */
|
|
15
|
+
code: string;
|
|
16
|
+
/** Insert after a specific resource (by name) */
|
|
17
|
+
afterResource?: {
|
|
18
|
+
type: StatementType;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface UpdateOptions {
|
|
23
|
+
/** The type of resource being updated */
|
|
24
|
+
resourceType: StatementType;
|
|
25
|
+
/** The resource name to update */
|
|
26
|
+
resourceName: string;
|
|
27
|
+
/** The new code for the resource (complete statement) */
|
|
28
|
+
newCode: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DeleteOptions {
|
|
31
|
+
/** The type of resource being deleted */
|
|
32
|
+
resourceType: StatementType;
|
|
33
|
+
/** The resource name to delete */
|
|
34
|
+
resourceName: string;
|
|
35
|
+
/** How to handle custom code that was after this resource */
|
|
36
|
+
orphanHandling: "preserve-with-warning" | "move-to-previous" | "delete";
|
|
37
|
+
}
|
|
38
|
+
export declare function addResourceSurgically(content: string, options: InsertionOptions): SurgicalModificationResult;
|
|
39
|
+
export declare function updateResourceSurgically(content: string, options: UpdateOptions): SurgicalModificationResult;
|
|
40
|
+
export declare function removeResourceSurgically(content: string, options: DeleteOptions): SurgicalModificationResult;
|
|
41
|
+
export declare function ensureImports(content: string, requiredImports: string[]): SurgicalModificationResult;
|
|
42
|
+
export declare function injectCustomCodeBlocks(generatedCode: string, customBlocks: CustomCodeBlock[], resourceMapping?: Map<string, string>): SurgicalModificationResult;
|
|
43
|
+
export interface FileValidationResult {
|
|
44
|
+
valid: boolean;
|
|
45
|
+
errors?: string[];
|
|
46
|
+
}
|
|
47
|
+
export declare function validateModifiedFile(content: string): FileValidationResult;
|
|
@@ -0,0 +1,400 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { type ParsedInfrastructure, type ParsedDatabaseResource, type ParsedS3Resource, type ParsedComputeResource, type ParsedSQSResource, type ParsedCDNResource, type ParsedNetworkResource, type ParsedDynamoDBResource, type ParsedLambdaConfig, type ParsedPatternResource, type ImportInfo, type StatementType, type ClassifiedStatement, type CustomCodeBlock, parseInfrastructure, convertToResourcePlan, classifyStatements, extractCustomCodeBlocks, findManagedResourcePosition, getLastManagedStatementOfType, getManagedResourcesByType, } from "./astInfrastructureParser.js";
|
|
2
|
+
export { type SurgicalModificationResult, type InsertionOptions, type UpdateOptions, type DeleteOptions, type FileValidationResult, addResourceSurgically, updateResourceSurgically, removeResourceSurgically, ensureImports, injectCustomCodeBlocks, validateModifiedFile, } from "./astSurgicalModification.js";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { parseInfrastructure, convertToResourcePlan, classifyStatements, extractCustomCodeBlocks, findManagedResourcePosition, getLastManagedStatementOfType, getManagedResourcesByType, } from "./astInfrastructureParser.js";
|
|
2
|
+
export { addResourceSurgically, updateResourceSurgically, removeResourceSurgically, ensureImports, injectCustomCodeBlocks, validateModifiedFile, } from "./astSurgicalModification.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface RegionInfo {
|
|
2
|
+
code: string;
|
|
3
|
+
name: string;
|
|
4
|
+
city: string;
|
|
5
|
+
country: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_REGION = "us-east-2";
|
|
8
|
+
export declare const regions: readonly string[];
|
|
9
|
+
export declare const AWS_REGIONS_METADATA: readonly RegionInfo[];
|
|
10
|
+
export declare const topRegions: readonly RegionInfo[];
|
|
11
|
+
export declare const commonRegions: readonly string[];
|
|
12
|
+
export declare function parseRegionList(value: string): string[];
|
|
13
|
+
export declare function isValidRegion(region: string): boolean;
|
|
14
|
+
export declare function isValidRegionFormat(region: string): boolean;
|
|
15
|
+
export declare function getSuggestions(input: string): string[];
|
|
16
|
+
export declare function validateRegion(region: string): true | string;
|
|
17
|
+
export declare function validateRegionList(value: string): string | boolean;
|
|
18
|
+
export declare function filterDuplicateRegions(regions: string[], primaryRegion: string): string[];
|
|
19
|
+
export declare function getRegionOptions(): Array<{
|
|
20
|
+
label: string;
|
|
21
|
+
value: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function getRegionOptionsExcluding(excludeCode: string): Array<{
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function getRegionName(code: string): string;
|
|
30
|
+
export declare function createRegionFormatter(primaryRegion: string): (value: string) => string[];
|