@synchronized-studio/cmsassets-agent 0.3.10 → 0.4.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.
@@ -0,0 +1,1178 @@
1
+ // src/planner/templates.ts
2
+ var PKG = "@synchronized-studio/response-transformer";
3
+ var CMS_FUNCTION_MAP = {
4
+ prismic: "transformPrismicAssetUrls",
5
+ contentful: "transformContentfulAssetUrls",
6
+ sanity: "transformSanityAssetUrls",
7
+ shopify: "transformShopifyAssetUrls",
8
+ cloudinary: "transformCloudinaryAssetUrls",
9
+ imgix: "transformImgixAssetUrls",
10
+ generic: "transformGenericAssetUrls"
11
+ };
12
+ var UNIFIED_FUNCTION = "transformCmsAssetUrls";
13
+ function getTransformFunctionName(cms) {
14
+ return CMS_FUNCTION_MAP[cms] ?? UNIFIED_FUNCTION;
15
+ }
16
+ function getImportStatement(cms) {
17
+ const fn = getTransformFunctionName(cms);
18
+ return `import { ${fn} } from '${PKG}'`;
19
+ }
20
+ var CMS_REQUIRED_PARAMS = {
21
+ prismic: [{ key: "repository", placeholder: "YOUR-REPO" }],
22
+ contentful: [{ key: "spaceId", placeholder: "YOUR-SPACE-ID" }],
23
+ sanity: [{ key: "projectId", placeholder: "YOUR-PROJECT-ID" }],
24
+ shopify: [{ key: "storeDomain", placeholder: "your-store.myshopify.com" }],
25
+ cloudinary: [{ key: "cloudName", placeholder: "YOUR-CLOUD-NAME" }],
26
+ imgix: [{ key: "imgixDomain", placeholder: "your-source.imgix.net" }],
27
+ generic: [{ key: "originUrl", placeholder: "https://your-origin.com" }]
28
+ };
29
+ function buildCmsOptions(cms, params, runtimeUrlExpr) {
30
+ const entries = [];
31
+ const required = CMS_REQUIRED_PARAMS[cms] ?? [];
32
+ for (const { key, placeholder } of required) {
33
+ const value = params[key];
34
+ if (value) {
35
+ entries.push(`${key}: '${value}'`);
36
+ } else {
37
+ entries.push(`${key}: '${placeholder}'`);
38
+ }
39
+ }
40
+ if (cms === "sanity" && params.dataset && params.dataset !== "production") {
41
+ entries.push(`dataset: '${params.dataset}'`);
42
+ }
43
+ if (runtimeUrlExpr) {
44
+ entries.push(`cmsAssetsUrl: ${runtimeUrlExpr}`);
45
+ }
46
+ if (entries.length === 0) return "{}";
47
+ if (entries.length === 1) {
48
+ return `{ ${entries[0]} }`;
49
+ }
50
+ return `{
51
+ ${entries.join(",\n ")},
52
+ }`;
53
+ }
54
+ function extractResJsonVarName(code) {
55
+ const m = code.match(/res\.json\(\s*(\w+)/);
56
+ return m?.[1] ?? "data";
57
+ }
58
+ var returnWrap = {
59
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
60
+ const fn = getTransformFunctionName(cms);
61
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
62
+ const lines = originalCode.split("\n");
63
+ lines[0] = lines[0].replace(/^return\s+/, "");
64
+ let expr = lines.join("\n").replace(/;?\s*$/, "").trim();
65
+ if (!expr) return originalCode;
66
+ return `return ${fn}(${expr}, ${opts})`;
67
+ },
68
+ description(cms) {
69
+ return `Wrap return value with ${getTransformFunctionName(cms)}()`;
70
+ }
71
+ };
72
+ var resJsonWrap = {
73
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
74
+ const varName = extractResJsonVarName(originalCode);
75
+ const fn = getTransformFunctionName(cms);
76
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
77
+ return originalCode.replace(
78
+ /res\.json\(\s*(\w+)\s*\)/,
79
+ `res.json(${fn}(${varName}, ${opts}))`
80
+ );
81
+ },
82
+ description(cms) {
83
+ return `Wrap res.json() argument with ${getTransformFunctionName(cms)}()`;
84
+ }
85
+ };
86
+ var useFetchTransformWrap = {
87
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
88
+ const fn = getTransformFunctionName(cms);
89
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
90
+ if (originalCode.includes(")")) {
91
+ const insertPos = originalCode.lastIndexOf(")");
92
+ const beforeClose = originalCode.substring(0, insertPos).trimEnd();
93
+ if (beforeClose.endsWith(",") || beforeClose.endsWith("(")) {
94
+ return `${beforeClose} { transform: (raw) => ${fn}(raw, ${opts}) })`;
95
+ }
96
+ return `${beforeClose}, { transform: (raw) => ${fn}(raw, ${opts}) })`;
97
+ }
98
+ return originalCode;
99
+ },
100
+ description(cms) {
101
+ return `Add transform option to useFetch() with ${getTransformFunctionName(cms)}()`;
102
+ }
103
+ };
104
+ var useAsyncDataTransformWrap = {
105
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
106
+ const fn = getTransformFunctionName(cms);
107
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
108
+ if (originalCode.includes(")")) {
109
+ const insertPos = originalCode.lastIndexOf(")");
110
+ const beforeClose = originalCode.substring(0, insertPos).trimEnd();
111
+ if (beforeClose.endsWith(",") || beforeClose.endsWith("(")) {
112
+ return `${beforeClose} { transform: (raw) => ${fn}(raw, ${opts}) })`;
113
+ }
114
+ return `${beforeClose}, { transform: (raw) => ${fn}(raw, ${opts}) })`;
115
+ }
116
+ return originalCode;
117
+ },
118
+ description(cms) {
119
+ return `Add transform option to useAsyncData() with ${getTransformFunctionName(cms)}()`;
120
+ }
121
+ };
122
+ var loaderReturnWrap = {
123
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
124
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
125
+ },
126
+ description(cms) {
127
+ return `Wrap Remix loader return with ${getTransformFunctionName(cms)}()`;
128
+ }
129
+ };
130
+ var getServerSidePropsWrap = {
131
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
132
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
133
+ },
134
+ description(cms) {
135
+ return `Wrap getServerSideProps return with ${getTransformFunctionName(cms)}()`;
136
+ }
137
+ };
138
+ var getStaticPropsWrap = {
139
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
140
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
141
+ },
142
+ description(cms) {
143
+ return `Wrap getStaticProps return with ${getTransformFunctionName(cms)}()`;
144
+ }
145
+ };
146
+ var loadReturnWrap = {
147
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
148
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
149
+ },
150
+ description(cms) {
151
+ return `Wrap SvelteKit load() return with ${getTransformFunctionName(cms)}()`;
152
+ }
153
+ };
154
+ var frontmatterAssignmentWrap = {
155
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
156
+ const fn = getTransformFunctionName(cms);
157
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
158
+ const m = originalCode.match(/((?:const|let)\s+\w+\s*=\s*)(await\s+.+)/);
159
+ if (m) {
160
+ return `${m[1]}${fn}(${m[2]}, ${opts})`;
161
+ }
162
+ return originalCode;
163
+ },
164
+ description(cms) {
165
+ return `Wrap Astro frontmatter assignment with ${getTransformFunctionName(cms)}()`;
166
+ }
167
+ };
168
+ var asyncDataReturnWrap = {
169
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
170
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
171
+ },
172
+ description(cms) {
173
+ return `Wrap Nuxt 2 asyncData return with ${getTransformFunctionName(cms)}()`;
174
+ }
175
+ };
176
+ var vuexActionReturnWrap = {
177
+ transform(originalCode, cms, params, _v, runtimeUrlExpr) {
178
+ return returnWrap.transform(originalCode, cms, params, void 0, runtimeUrlExpr);
179
+ },
180
+ description(cms) {
181
+ return `Wrap Vuex action return with ${getTransformFunctionName(cms)}()`;
182
+ }
183
+ };
184
+ var vuexCommitWrap = {
185
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
186
+ const fn = getTransformFunctionName(cms);
187
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
188
+ const valueMatch = originalCode.match(/(value\s*:\s*)([^,}]+)/);
189
+ if (!valueMatch) return originalCode;
190
+ const valueExpr = valueMatch[2].trim();
191
+ return originalCode.replace(
192
+ valueMatch[0],
193
+ `${valueMatch[1]}${fn}(${valueExpr}, ${opts})`
194
+ );
195
+ },
196
+ description(cms) {
197
+ return `Wrap Vuex commit value with ${getTransformFunctionName(cms)}()`;
198
+ }
199
+ };
200
+ var WRAP_TEMPLATES = {
201
+ "return": returnWrap,
202
+ "res.json": resJsonWrap,
203
+ "useFetch-transform": useFetchTransformWrap,
204
+ "useAsyncData-transform": useAsyncDataTransformWrap,
205
+ "loader-return": loaderReturnWrap,
206
+ "getServerSideProps-return": getServerSidePropsWrap,
207
+ "getStaticProps-return": getStaticPropsWrap,
208
+ "load-return": loadReturnWrap,
209
+ "frontmatter-assignment": frontmatterAssignmentWrap,
210
+ "asyncData-return": asyncDataReturnWrap,
211
+ "vuex-action-return": vuexActionReturnWrap,
212
+ "vuex-commit": vuexCommitWrap,
213
+ "assignment": {
214
+ transform(originalCode, cms, params, _varName, runtimeUrlExpr) {
215
+ const fn = getTransformFunctionName(cms);
216
+ const opts = buildCmsOptions(cms, params, runtimeUrlExpr);
217
+ const m = originalCode.match(/((?:const|let|var)\s+\w+\s*=\s*)(.+)/);
218
+ if (m) return `${m[1]}${fn}(${m[2]}, ${opts})`;
219
+ return originalCode;
220
+ },
221
+ description(cms) {
222
+ return `Wrap assignment with ${getTransformFunctionName(cms)}()`;
223
+ }
224
+ }
225
+ };
226
+
227
+ // src/scanner/findInjectionPoints.ts
228
+ import { readFileSync } from "fs";
229
+ import { relative } from "path";
230
+ import fg from "fast-glob";
231
+ var IGNORE_DIRS = [
232
+ "node_modules",
233
+ ".nuxt",
234
+ ".next",
235
+ ".output",
236
+ ".svelte-kit",
237
+ "dist",
238
+ "build",
239
+ ".git",
240
+ "coverage",
241
+ ".cache"
242
+ ];
243
+ var TEST_PATTERNS = [
244
+ /\.test\.[jt]sx?$/,
245
+ /\.spec\.[jt]sx?$/,
246
+ /__tests__\//,
247
+ /\/tests?\//,
248
+ /\.stories\.[jt]sx?$/,
249
+ /\/fixtures?\//,
250
+ /\/mocks?\//
251
+ ];
252
+ var CMS_FETCH_PATTERNS = [
253
+ /prismic/i,
254
+ /contentful/i,
255
+ /sanity/i,
256
+ /shopify/i,
257
+ /cloudinary/i,
258
+ /imgix/i,
259
+ /ctfassets/i,
260
+ /cdn\.prismic/i,
261
+ /cdn\.sanity/i,
262
+ /myshopify/i,
263
+ /res\.cloudinary/i
264
+ ];
265
+ function hasCmsFetchSignal(content) {
266
+ const stripped = content.split("\n").map((line) => {
267
+ if (/^\s*(import\s|.*\brequire\s*\()/.test(line)) return line;
268
+ return line.replace(/(['"`])(?:(?!\1)[^\\]|\\.)*?\1/g, "$1$1");
269
+ }).join("\n");
270
+ return CMS_FETCH_PATTERNS.some((p) => p.test(stripped));
271
+ }
272
+ function isTestFile(filePath) {
273
+ return TEST_PATTERNS.some((p) => p.test(filePath));
274
+ }
275
+ function computeScore(signals) {
276
+ let score = 0;
277
+ if (signals.hasCmsFetch) score += 40;
278
+ if (signals.isServerSide) score += 30;
279
+ if (signals.returnsJson) score += 20;
280
+ if (signals.isCentralLayer) score += 10;
281
+ if (signals.isTest) score -= 20;
282
+ return Math.max(0, Math.min(100, score));
283
+ }
284
+ function scoreToConfidence(score) {
285
+ if (score >= 80) return "high";
286
+ if (score >= 60) return "medium";
287
+ return "low";
288
+ }
289
+ function buildReasons(signals) {
290
+ const reasons = [];
291
+ if (signals.hasCmsFetch) reasons.push("File contains CMS SDK import or fetch to CMS domain");
292
+ if (signals.isServerSide) reasons.push("Server-side file (API route, loader, SSR handler)");
293
+ if (signals.returnsJson) reasons.push("Returns JSON data (return statement / res.json)");
294
+ if (signals.isCentralLayer) reasons.push("Centralized data layer (composable, service, helper)");
295
+ if (signals.isTest) reasons.push("Test/fixture file (score reduced)");
296
+ return reasons;
297
+ }
298
+ function getContextLines(content, line, range = 2) {
299
+ const lines = content.split("\n");
300
+ const start = Math.max(0, line - 1 - range);
301
+ const end = Math.min(lines.length, line + range);
302
+ return lines.slice(start, end).join("\n");
303
+ }
304
+ function findReturnLineInBody(lines, startLineIndex) {
305
+ let braceDepth = 0;
306
+ for (let i = startLineIndex; i < lines.length; i++) {
307
+ const line = lines[i];
308
+ const trimmed = line.trim();
309
+ for (const c of line) {
310
+ if (c === "{") braceDepth++;
311
+ else if (c === "}") braceDepth--;
312
+ }
313
+ if (braceDepth > 0 && /^return\s/.test(trimmed) && looksLikeDataReturn(trimmed)) {
314
+ if (isFactoryReturn(lines, i, trimmed)) continue;
315
+ if (/[;}]\s*$/.test(trimmed) || trimmed.includes("}") && trimmed.indexOf("}") > trimmed.indexOf("{")) {
316
+ return { lineIndex: i, trimmed };
317
+ }
318
+ let returnBrace = 0;
319
+ const parts = [];
320
+ for (let j = i; j < lines.length; j++) {
321
+ const ln = lines[j];
322
+ parts.push(ln);
323
+ for (const c of ln) {
324
+ if (c === "{") returnBrace++;
325
+ else if (c === "}") returnBrace--;
326
+ }
327
+ if (returnBrace === 0) {
328
+ return { lineIndex: i, trimmed: parts.join("\n").trim() };
329
+ }
330
+ }
331
+ return { lineIndex: i, trimmed };
332
+ }
333
+ if (braceDepth === 0 && i > startLineIndex) break;
334
+ }
335
+ return null;
336
+ }
337
+ function expandToFullCall(lines, startLineIndex) {
338
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
339
+ const firstLine = lines[startLineIndex];
340
+ const match = firstLine.match(pattern);
341
+ if (!match) return firstLine.trim();
342
+ const fnName = match[1];
343
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
344
+ if (openParenIdx === -1) return firstLine.trim();
345
+ let depth = 1;
346
+ const parts = [];
347
+ for (let i = startLineIndex; i < lines.length; i++) {
348
+ const line = lines[i];
349
+ parts.push(line);
350
+ const full = parts.join("\n");
351
+ const scanStart = i === startLineIndex ? openParenIdx + 1 : full.length - line.length;
352
+ for (let k = scanStart; k < full.length; k++) {
353
+ const c = full[k];
354
+ if (c === "(") depth++;
355
+ else if (c === ")") {
356
+ depth--;
357
+ if (depth === 0) return full.substring(0, k + 1);
358
+ }
359
+ }
360
+ }
361
+ return parts.join("\n").trim();
362
+ }
363
+ function findCallEndLine(lines, startLineIndex) {
364
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
365
+ const firstLine = lines[startLineIndex];
366
+ const match = firstLine.match(pattern);
367
+ if (!match) return startLineIndex + 1;
368
+ const fnName = match[1];
369
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
370
+ if (openParenIdx === -1) return startLineIndex + 1;
371
+ let depth = 1;
372
+ for (let i = startLineIndex; i < lines.length; i++) {
373
+ const line = lines[i];
374
+ const startCol = i === startLineIndex ? openParenIdx + 1 : 0;
375
+ for (let k = startCol; k < line.length; k++) {
376
+ const c = line[k];
377
+ if (c === "(") depth++;
378
+ else if (c === ")") {
379
+ depth--;
380
+ if (depth === 0) return i + 1;
381
+ }
382
+ }
383
+ }
384
+ return startLineIndex + 1;
385
+ }
386
+ var NON_DATA_RETURN_PATTERNS = [
387
+ /^return\s+(true|false|null|undefined|void)\b/,
388
+ /^return\s+["'`]/,
389
+ // return "string"
390
+ /^return\s+\d/,
391
+ // return 42
392
+ /^return\s+\w+\s*(===|!==|==|!=|>|<|>=|<=)/,
393
+ // return x === "true"
394
+ /^return\s+!\w/,
395
+ // return !value
396
+ /^return\s*$/,
397
+ // bare return
398
+ /^return\s*;?\s*$/,
399
+ // return;
400
+ /^return\s+err\b/,
401
+ // return err / error
402
+ /^return\s+error\b/,
403
+ /^return\s+\[\s*\]/,
404
+ // return []
405
+ /^return\s+\{\s*\}/,
406
+ // return {}
407
+ /^return\s+dispatch\s*\(/,
408
+ // return dispatch(...) -- Vuex forwarding
409
+ /^return\s+commit\s*\(/,
410
+ // return commit(...)
411
+ /^return\s+(?:this\.\$store\.)?dispatch\s*\(/,
412
+ // return this.$store.dispatch(...)
413
+ /^return\s+store\.dispatch\s*\(/,
414
+ // return store.dispatch(...)
415
+ /^return\s+_get\s*\(/,
416
+ // return _get(...) -- lodash getter
417
+ /^return\s+_\w+\s*\(/,
418
+ // return _clone(...) etc -- lodash util
419
+ /^return\s+\w+\s*\?\s*/,
420
+ // return x ? y : z -- ternary
421
+ /^return\s+\w+\.\w+\s*(===|!==|==|!=)/,
422
+ // return link.link_type === "Web"
423
+ /^return\s+(axios|fetch|http|request)\b/,
424
+ // return axios / return fetch (the promise, not data)
425
+ /^return\s+res\.(json|send|status)\s*\(/,
426
+ // return res.json(...) -- handled separately as res.json type
427
+ /^return\s+"#"/
428
+ // return "#"
429
+ ];
430
+ var CALLBACK_PROPERTY_PATTERNS = [
431
+ /shouldBypassCache/,
432
+ /shouldInvalidateCache/,
433
+ /getKey/,
434
+ /onRequest/,
435
+ /onResponse/,
436
+ /onError/,
437
+ /transform\s*:/,
438
+ /validate\s*:/
439
+ ];
440
+ function isInsideCallbackOption(lines, returnLineIdx) {
441
+ for (let i = returnLineIdx - 1; i >= Math.max(0, returnLineIdx - 15); i--) {
442
+ const line = lines[i].trim();
443
+ if (CALLBACK_PROPERTY_PATTERNS.some((p) => p.test(line))) return true;
444
+ if (/^(export\s+default|async\s+function|function\s)/.test(line)) break;
445
+ if (/^(export\s+default\s+)?define\w+Handler/.test(line)) break;
446
+ }
447
+ return false;
448
+ }
449
+ function looksLikeDataReturn(trimmed) {
450
+ if (NON_DATA_RETURN_PATTERNS.some((p) => p.test(trimmed))) return false;
451
+ if (/^return\s+\{\s*\w/.test(trimmed)) return true;
452
+ if (/^return\s+\{\s*$/.test(trimmed)) return true;
453
+ if (/^return\s+\[\s*\w/.test(trimmed)) return true;
454
+ if (/^return\s+await\s+\w+\.\w+/.test(trimmed)) return true;
455
+ if (/^return\s+[a-zA-Z_]\w*\s*;?\s*$/.test(trimmed)) return true;
456
+ if (/^return\s+\(?[a-zA-Z_]\w*\)?\s+as\s+[\w<>{}\[\]|&.,\s]+;?\s*$/.test(trimmed)) return true;
457
+ if (/^return\s+\(\s*[a-zA-Z_]\w*\s+as\s+[\w<>{}\[\]|&.,\s]+\s*\)\s*;?\s*$/.test(trimmed)) return true;
458
+ if (/\.push\s*\(|\.splice\s*\(|\.concat\s*\(/.test(trimmed)) return false;
459
+ if (/^return\s+Response\.json\s*\(/.test(trimmed)) return true;
460
+ if (/^return\s+json\s*\(/.test(trimmed)) return true;
461
+ if (/^return\s+new\s+Response/.test(trimmed)) return true;
462
+ return false;
463
+ }
464
+ function extractShorthandProps(returnCode) {
465
+ const match = returnCode.match(/^return\s+\{([^}]+)\}\s*;?\s*$/);
466
+ if (!match) return null;
467
+ const inner = match[1].trim();
468
+ const parts = inner.split(",").map((p) => p.trim()).filter(Boolean);
469
+ const names = [];
470
+ for (const part of parts) {
471
+ if (/^\w+$/.test(part)) {
472
+ names.push(part);
473
+ } else {
474
+ return null;
475
+ }
476
+ }
477
+ return names.length > 0 ? names : null;
478
+ }
479
+ var FUNCTION_DECLARATION_PATTERNS = [
480
+ /^(?:const|let)\s+NAME\s*=\s*cache\s*\(/,
481
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?\(/,
482
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?(?:\w+\s*=>\s*|function)/,
483
+ /^(?:async\s+)?function\s+NAME\s*\(/,
484
+ // Vue reactive wrappers: computed/ref/reactive produce non-serializable objects
485
+ /^(?:const|let)\s+NAME\s*=\s*(?:computed|ref|reactive|shallowRef|shallowReactive|toRef|toRefs)\s*\(/
486
+ ];
487
+ function isFactoryReturn(lines, returnLineIdx, returnCode) {
488
+ const props = extractShorthandProps(returnCode);
489
+ if (!props || props.length === 0) return false;
490
+ const searchStart = Math.max(0, returnLineIdx - 200);
491
+ const scope = lines.slice(searchStart, returnLineIdx);
492
+ return props.every((name) => {
493
+ const patterns = FUNCTION_DECLARATION_PATTERNS.map(
494
+ (p) => new RegExp(p.source.replace("NAME", name))
495
+ );
496
+ return scope.some((line) => {
497
+ const trimmed = line.trim();
498
+ return patterns.some((p) => p.test(trimmed));
499
+ });
500
+ });
501
+ }
502
+ function findCommitInBody(lines, startLineIndex) {
503
+ let braceDepth = 0;
504
+ for (let i = startLineIndex; i < lines.length; i++) {
505
+ const line = lines[i];
506
+ const trimmed = line.trim();
507
+ for (const c of line) {
508
+ if (c === "{") braceDepth++;
509
+ else if (c === "}") braceDepth--;
510
+ }
511
+ if (braceDepth === 0 && i > startLineIndex) break;
512
+ if (braceDepth > 0 && /\bcommit\s*\(/.test(trimmed) && /\bvalue\s*:/.test(trimmed)) {
513
+ if (/[)]\s*$/.test(trimmed)) {
514
+ return { lineIndex: i, fullStatement: trimmed };
515
+ }
516
+ let parenDepth = 0;
517
+ const parts = [];
518
+ for (let j = i; j < lines.length; j++) {
519
+ parts.push(lines[j]);
520
+ for (const c of lines[j]) {
521
+ if (c === "(") parenDepth++;
522
+ else if (c === ")") parenDepth--;
523
+ }
524
+ if (parenDepth <= 0) {
525
+ return { lineIndex: i, fullStatement: parts.join("\n").trim() };
526
+ }
527
+ }
528
+ return { lineIndex: i, fullStatement: trimmed };
529
+ }
530
+ if (braceDepth > 0 && (/\bcommit\s*\(\s*$/.test(trimmed) || /\bcommit\s*\(\s*["']/.test(trimmed))) {
531
+ let parenDepth = 0;
532
+ const parts = [];
533
+ let hasValue = false;
534
+ for (let j = i; j < Math.min(lines.length, i + 20); j++) {
535
+ parts.push(lines[j]);
536
+ if (/\bvalue\s*:/.test(lines[j])) hasValue = true;
537
+ for (const c of lines[j]) {
538
+ if (c === "(") parenDepth++;
539
+ else if (c === ")") parenDepth--;
540
+ }
541
+ if (parenDepth <= 0) {
542
+ if (hasValue) {
543
+ return { lineIndex: i, fullStatement: parts.join("\n").trim() };
544
+ }
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ function findReturnStatements(content, filePath, cms, root, opts) {
553
+ const candidates = [];
554
+ const lines = content.split("\n");
555
+ const hasCms = hasCmsFetchSignal(content);
556
+ const relPath = relative(root, filePath);
557
+ const isTest = isTestFile(relPath);
558
+ for (let i = 0; i < lines.length; i++) {
559
+ const line = lines[i];
560
+ const trimmed = line.trim();
561
+ if (/^return\s/.test(trimmed)) {
562
+ if (!looksLikeDataReturn(trimmed)) continue;
563
+ if (isInsideCallbackOption(lines, i)) continue;
564
+ let fullReturn = trimmed;
565
+ if (/^return\s+\{\s*$/.test(trimmed)) {
566
+ let braceCount = 0;
567
+ const parts = [];
568
+ for (let j = i; j < lines.length; j++) {
569
+ parts.push(lines[j]);
570
+ for (const c of lines[j]) {
571
+ if (c === "{") braceCount++;
572
+ else if (c === "}") braceCount--;
573
+ }
574
+ if (braceCount === 0) break;
575
+ }
576
+ fullReturn = parts.join("\n").trim();
577
+ }
578
+ if (isFactoryReturn(lines, i, fullReturn)) continue;
579
+ if (/^return\s+\{/.test(fullReturn) && !hasCms && !opts.isServerSide) continue;
580
+ const signals = {
581
+ hasCmsFetch: hasCms,
582
+ isServerSide: opts.isServerSide,
583
+ returnsJson: true,
584
+ isCentralLayer: opts.isCentralLayer,
585
+ isTest
586
+ };
587
+ const score = computeScore(signals);
588
+ if (score < 50 || !signals.hasCmsFetch && !signals.isServerSide) {
589
+ continue;
590
+ }
591
+ candidates.push({
592
+ filePath: relative(root, filePath),
593
+ line: i + 1,
594
+ type: "return",
595
+ score,
596
+ confidence: scoreToConfidence(score),
597
+ targetCode: fullReturn,
598
+ context: getContextLines(content, i + 1),
599
+ reasons: buildReasons(signals)
600
+ });
601
+ }
602
+ if (/res\.json\s*\(/.test(trimmed)) {
603
+ if (isInsideCallbackOption(lines, i)) continue;
604
+ const signals = {
605
+ hasCmsFetch: hasCms,
606
+ isServerSide: true,
607
+ returnsJson: true,
608
+ isCentralLayer: opts.isCentralLayer,
609
+ isTest
610
+ };
611
+ const score = computeScore(signals);
612
+ if (score < 50) {
613
+ continue;
614
+ }
615
+ candidates.push({
616
+ filePath: relative(root, filePath),
617
+ line: i + 1,
618
+ type: "res.json",
619
+ score,
620
+ confidence: scoreToConfidence(score),
621
+ targetCode: trimmed,
622
+ context: getContextLines(content, i + 1),
623
+ reasons: buildReasons(signals)
624
+ });
625
+ }
626
+ }
627
+ return candidates;
628
+ }
629
+ var nuxtStrategy = {
630
+ globs: [
631
+ "server/api/**/*.ts",
632
+ "server/api/**/*.js",
633
+ "server/routes/**/*.ts",
634
+ "composables/**/*.ts",
635
+ "composables/**/*.js",
636
+ "pages/**/*.vue",
637
+ "pages/**/*.ts"
638
+ ],
639
+ analyze(filePath, content, cms, root) {
640
+ const relPath = relative(root, filePath);
641
+ const isServerApi = /server\/(api|routes)\//.test(filePath);
642
+ const isComposable = /composables\//.test(filePath);
643
+ const candidates = findReturnStatements(content, filePath, cms, root, {
644
+ isServerSide: isServerApi,
645
+ isCentralLayer: isComposable
646
+ });
647
+ const lines = content.split("\n");
648
+ const callbackReturnRanges = [];
649
+ for (let i = 0; i < lines.length; i++) {
650
+ const trimmed = lines[i].trim();
651
+ if (/\buseFetch\s*\(/.test(trimmed) && !/transform\s*:/.test(content.substring(content.indexOf(trimmed)))) {
652
+ const hasCms = hasCmsFetchSignal(content);
653
+ if (!hasCms) continue;
654
+ const signals = {
655
+ hasCmsFetch: hasCms,
656
+ isServerSide: false,
657
+ returnsJson: true,
658
+ isCentralLayer: isComposable,
659
+ isTest: isTestFile(relPath)
660
+ };
661
+ const score = computeScore(signals);
662
+ if (score < 50) continue;
663
+ candidates.push({
664
+ filePath: relPath,
665
+ line: i + 1,
666
+ type: "useFetch-transform",
667
+ score,
668
+ confidence: scoreToConfidence(score),
669
+ targetCode: expandToFullCall(lines, i),
670
+ context: getContextLines(content, i + 1),
671
+ reasons: [...buildReasons(signals), "useFetch call without transform option"]
672
+ });
673
+ }
674
+ if (/\buseAsyncData\s*\(/.test(trimmed)) {
675
+ const fullCall = expandToFullCall(lines, i);
676
+ if (/transform\s*:/.test(fullCall)) continue;
677
+ const hasCms = hasCmsFetchSignal(content);
678
+ if (!hasCms) continue;
679
+ callbackReturnRanges.push({
680
+ start: i + 1,
681
+ end: findCallEndLine(lines, i)
682
+ });
683
+ const signals = {
684
+ hasCmsFetch: hasCms,
685
+ isServerSide: false,
686
+ returnsJson: true,
687
+ isCentralLayer: isComposable,
688
+ isTest: isTestFile(relPath)
689
+ };
690
+ const score = computeScore(signals) + (/pages\//.test(filePath) ? 10 : 0);
691
+ if (score < 50) continue;
692
+ candidates.push({
693
+ filePath: relPath,
694
+ line: i + 1,
695
+ type: "useAsyncData-transform",
696
+ score: Math.min(100, score),
697
+ confidence: scoreToConfidence(score),
698
+ targetCode: fullCall,
699
+ context: getContextLines(content, i + 1),
700
+ reasons: [...buildReasons(signals), "useAsyncData call"]
701
+ });
702
+ }
703
+ }
704
+ const filteredByCallback = callbackReturnRanges.length === 0 ? candidates : candidates.filter((candidate) => {
705
+ if (candidate.type !== "return") return true;
706
+ return !callbackReturnRanges.some(
707
+ (range) => candidate.line > range.start && candidate.line <= range.end
708
+ );
709
+ });
710
+ if (filePath.endsWith(".vue")) {
711
+ const hasDataTransformTarget = filteredByCallback.some(
712
+ (candidate) => candidate.type === "useAsyncData-transform" || candidate.type === "useFetch-transform"
713
+ );
714
+ if (hasDataTransformTarget) {
715
+ return filteredByCallback.filter((candidate) => candidate.type !== "return");
716
+ }
717
+ }
718
+ return filteredByCallback;
719
+ }
720
+ };
721
+ var nextStrategy = {
722
+ globs: [
723
+ "app/**/page.tsx",
724
+ "app/**/page.ts",
725
+ "app/**/page.jsx",
726
+ "app/**/page.js",
727
+ "app/api/**/route.ts",
728
+ "app/api/**/route.js",
729
+ "pages/**/*.tsx",
730
+ "pages/**/*.ts",
731
+ "pages/**/*.jsx",
732
+ "pages/**/*.js",
733
+ "src/app/**/page.tsx",
734
+ "src/app/api/**/route.ts",
735
+ "src/pages/**/*.tsx",
736
+ "src/pages/**/*.ts",
737
+ "lib/**/*.ts",
738
+ "lib/**/*.js",
739
+ "src/lib/**/*.ts",
740
+ "src/lib/**/*.js"
741
+ ],
742
+ analyze(filePath, content, cms, root) {
743
+ const relPath = relative(root, filePath);
744
+ const isApiRoute = /\/api\//.test(filePath) && /route\.[jt]sx?$/.test(filePath);
745
+ const isAppRouterPage = /(^|\/)(src\/)?app\/(?:.*\/)?page\.[jt]sx?$/.test(relPath);
746
+ const isLib = /\blib\//.test(filePath);
747
+ const candidates = isAppRouterPage ? [] : findReturnStatements(content, filePath, cms, root, {
748
+ isServerSide: isApiRoute || /getServerSideProps|getStaticProps/.test(content),
749
+ isCentralLayer: isLib
750
+ });
751
+ const lines = content.split("\n");
752
+ for (let i = 0; i < lines.length; i++) {
753
+ const trimmed = lines[i].trim();
754
+ if (/export\s+(async\s+)?function\s+getServerSideProps/.test(trimmed)) {
755
+ const signals = {
756
+ hasCmsFetch: hasCmsFetchSignal(content),
757
+ isServerSide: true,
758
+ returnsJson: true,
759
+ isCentralLayer: false,
760
+ isTest: isTestFile(relPath)
761
+ };
762
+ const score = computeScore(signals);
763
+ candidates.push({
764
+ filePath: relPath,
765
+ line: i + 1,
766
+ type: "getServerSideProps-return",
767
+ score,
768
+ confidence: scoreToConfidence(score),
769
+ targetCode: trimmed,
770
+ context: getContextLines(content, i + 1),
771
+ reasons: buildReasons(signals)
772
+ });
773
+ }
774
+ if (/export\s+(async\s+)?function\s+getStaticProps/.test(trimmed)) {
775
+ const signals = {
776
+ hasCmsFetch: hasCmsFetchSignal(content),
777
+ isServerSide: true,
778
+ returnsJson: true,
779
+ isCentralLayer: false,
780
+ isTest: isTestFile(relPath)
781
+ };
782
+ const score = computeScore(signals);
783
+ candidates.push({
784
+ filePath: relPath,
785
+ line: i + 1,
786
+ type: "getStaticProps-return",
787
+ score,
788
+ confidence: scoreToConfidence(score),
789
+ targetCode: trimmed,
790
+ context: getContextLines(content, i + 1),
791
+ reasons: buildReasons(signals)
792
+ });
793
+ }
794
+ }
795
+ return candidates;
796
+ }
797
+ };
798
+ var remixStrategy = {
799
+ globs: ["app/routes/**/*.tsx", "app/routes/**/*.ts", "app/routes/**/*.jsx", "app/routes/**/*.js"],
800
+ analyze(filePath, content, cms, root) {
801
+ const relPath = relative(root, filePath);
802
+ const candidates = [];
803
+ const lines = content.split("\n");
804
+ for (let i = 0; i < lines.length; i++) {
805
+ const trimmed = lines[i].trim();
806
+ if (/export\s+(async\s+)?function\s+loader/.test(trimmed) || /export\s+const\s+loader/.test(trimmed)) {
807
+ const signals = {
808
+ hasCmsFetch: hasCmsFetchSignal(content),
809
+ isServerSide: true,
810
+ returnsJson: true,
811
+ isCentralLayer: false,
812
+ isTest: isTestFile(relPath)
813
+ };
814
+ const score = computeScore(signals);
815
+ candidates.push({
816
+ filePath: relPath,
817
+ line: i + 1,
818
+ type: "loader-return",
819
+ score,
820
+ confidence: scoreToConfidence(score),
821
+ targetCode: trimmed,
822
+ context: getContextLines(content, i + 1, 5),
823
+ reasons: [...buildReasons(signals), "Remix loader function"]
824
+ });
825
+ }
826
+ }
827
+ candidates.push(
828
+ ...findReturnStatements(content, filePath, cms, root, {
829
+ isServerSide: true,
830
+ isCentralLayer: false
831
+ })
832
+ );
833
+ return candidates;
834
+ }
835
+ };
836
+ var astroStrategy = {
837
+ globs: ["src/pages/**/*.astro", "src/pages/api/**/*.ts", "src/pages/api/**/*.js", "src/lib/**/*.ts"],
838
+ analyze(filePath, content, cms, root) {
839
+ const isApi = /\/api\//.test(filePath);
840
+ const isLib = /\blib\//.test(filePath);
841
+ if (filePath.endsWith(".astro")) {
842
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
843
+ if (frontmatterMatch) {
844
+ const frontmatter = frontmatterMatch[1];
845
+ if (hasCmsFetchSignal(frontmatter)) {
846
+ const assignmentPattern = /(?:const|let)\s+(\w+)\s*=\s*await\s+/g;
847
+ let m;
848
+ const candidates = [];
849
+ while ((m = assignmentPattern.exec(frontmatter)) !== null) {
850
+ const linesBefore = frontmatter.substring(0, m.index).split("\n").length;
851
+ const signals = {
852
+ hasCmsFetch: true,
853
+ isServerSide: true,
854
+ returnsJson: true,
855
+ isCentralLayer: false,
856
+ isTest: false
857
+ };
858
+ const score = computeScore(signals);
859
+ const matchedLine = frontmatter.split("\n")[linesBefore]?.trim() ?? m[0].trim();
860
+ candidates.push({
861
+ filePath: relative(root, filePath),
862
+ line: linesBefore + 1,
863
+ type: "frontmatter-assignment",
864
+ score,
865
+ confidence: scoreToConfidence(score),
866
+ targetCode: matchedLine,
867
+ context: getContextLines(frontmatter, linesBefore + 1),
868
+ reasons: [...buildReasons(signals), "Astro frontmatter data fetch"]
869
+ });
870
+ }
871
+ return candidates;
872
+ }
873
+ }
874
+ return [];
875
+ }
876
+ return findReturnStatements(content, filePath, cms, root, {
877
+ isServerSide: isApi,
878
+ isCentralLayer: isLib
879
+ });
880
+ }
881
+ };
882
+ var sveltekitStrategy = {
883
+ globs: [
884
+ "src/routes/**/+page.server.ts",
885
+ "src/routes/**/+page.server.js",
886
+ "src/routes/**/+server.ts",
887
+ "src/routes/**/+server.js",
888
+ "src/routes/**/+layout.server.ts",
889
+ "src/routes/**/+layout.server.js",
890
+ "src/lib/**/*.ts",
891
+ "src/lib/**/*.js"
892
+ ],
893
+ analyze(filePath, content, cms, root) {
894
+ const relPath = relative(root, filePath);
895
+ const isServer = /\+(?:page|layout)\.server\.[jt]s$/.test(filePath) || /\+server\.[jt]s$/.test(filePath);
896
+ const isLib = /src\/lib\//.test(filePath);
897
+ const candidates = [];
898
+ const lines = content.split("\n");
899
+ for (let i = 0; i < lines.length; i++) {
900
+ const trimmed = lines[i].trim();
901
+ if (/export\s+(async\s+)?function\s+load/.test(trimmed) || /export\s+const\s+load/.test(trimmed)) {
902
+ const signals = {
903
+ hasCmsFetch: hasCmsFetchSignal(content),
904
+ isServerSide: isServer,
905
+ returnsJson: true,
906
+ isCentralLayer: isLib,
907
+ isTest: isTestFile(relPath)
908
+ };
909
+ const score = computeScore(signals);
910
+ candidates.push({
911
+ filePath: relPath,
912
+ line: i + 1,
913
+ type: "load-return",
914
+ score,
915
+ confidence: scoreToConfidence(score),
916
+ targetCode: trimmed,
917
+ context: getContextLines(content, i + 1, 5),
918
+ reasons: [...buildReasons(signals), "SvelteKit load function"]
919
+ });
920
+ }
921
+ }
922
+ candidates.push(
923
+ ...findReturnStatements(content, filePath, cms, root, {
924
+ isServerSide: isServer,
925
+ isCentralLayer: isLib
926
+ })
927
+ );
928
+ return candidates;
929
+ }
930
+ };
931
+ var nuxt2Strategy = {
932
+ globs: [
933
+ "store/**/*.js",
934
+ "store/**/*.ts",
935
+ "pages/**/*.vue",
936
+ "mixins/**/*.js",
937
+ "mixins/**/*.ts",
938
+ "services/**/*.js",
939
+ "services/**/*.ts",
940
+ "api/**/*.js",
941
+ "api/**/*.ts",
942
+ "plugins/**/*.js",
943
+ "plugins/**/*.ts"
944
+ ],
945
+ analyze(filePath, content, cms, root) {
946
+ const relPath = relative(root, filePath);
947
+ const isStore = /store\//.test(filePath);
948
+ const isService = /services\//.test(filePath);
949
+ const isApi = /\bapi\//.test(filePath);
950
+ const isPage = /pages\//.test(filePath);
951
+ const candidates = [];
952
+ const hasCms = hasCmsFetchSignal(content);
953
+ const isTest = isTestFile(relPath);
954
+ const lines = content.split("\n");
955
+ if (isPage && filePath.endsWith(".vue")) {
956
+ for (let i = 0; i < lines.length; i++) {
957
+ const trimmed = lines[i].trim();
958
+ if (/async\s+asyncData\s*\(/.test(trimmed) || /asyncData\s*\(\s*\{/.test(trimmed)) {
959
+ const ret = findReturnLineInBody(lines, i);
960
+ if (!ret) continue;
961
+ const signals = {
962
+ hasCmsFetch: hasCms || /\$prismic|\$contentful|\$sanity/.test(content),
963
+ isServerSide: true,
964
+ returnsJson: true,
965
+ isCentralLayer: false,
966
+ isTest
967
+ };
968
+ const score = computeScore(signals);
969
+ if (score >= 50) {
970
+ candidates.push({
971
+ filePath: relPath,
972
+ line: ret.lineIndex + 1,
973
+ type: "asyncData-return",
974
+ score,
975
+ confidence: scoreToConfidence(score),
976
+ targetCode: ret.trimmed,
977
+ context: getContextLines(content, ret.lineIndex + 1, 5),
978
+ reasons: [...buildReasons(signals), "Nuxt 2 asyncData hook"]
979
+ });
980
+ }
981
+ }
982
+ }
983
+ }
984
+ if (isStore) {
985
+ const cmsCallPatterns = [
986
+ /this\.\$prismic/,
987
+ /this\.\$contentful/,
988
+ /this\.\$sanity/,
989
+ /client\.get/,
990
+ /client\.getSingle/,
991
+ /client\.getByUID/,
992
+ /client\.getAllByType/,
993
+ /client\.fetch/
994
+ ];
995
+ for (let i = 0; i < lines.length; i++) {
996
+ const trimmed = lines[i].trim();
997
+ if (!/^async\s+\w+\s*\(/.test(trimmed)) continue;
998
+ let bodyHasCmsCall = false;
999
+ for (let j = i + 1; j < Math.min(lines.length, i + 50); j++) {
1000
+ const bodyLine = lines[j];
1001
+ if (/^\s*(async\s+)?\w+\s*\(/.test(bodyLine) && j > i + 1 && !bodyLine.trim().startsWith("//")) {
1002
+ if (/^\s{0,4}(async\s+)?\w+\s*\(\s*\{/.test(bodyLine)) break;
1003
+ }
1004
+ if (cmsCallPatterns.some((p) => p.test(bodyLine))) {
1005
+ bodyHasCmsCall = true;
1006
+ break;
1007
+ }
1008
+ }
1009
+ if (!bodyHasCmsCall) continue;
1010
+ const signals = {
1011
+ hasCmsFetch: true,
1012
+ isServerSide: true,
1013
+ returnsJson: true,
1014
+ isCentralLayer: true,
1015
+ isTest
1016
+ };
1017
+ const score = computeScore(signals);
1018
+ const ret = findReturnLineInBody(lines, i);
1019
+ const commitCall = findCommitInBody(lines, i);
1020
+ if (ret) {
1021
+ candidates.push({
1022
+ filePath: relPath,
1023
+ line: ret.lineIndex + 1,
1024
+ type: "vuex-action-return",
1025
+ score,
1026
+ confidence: scoreToConfidence(score),
1027
+ targetCode: ret.trimmed,
1028
+ context: getContextLines(content, ret.lineIndex + 1, 8),
1029
+ reasons: [...buildReasons(signals), "Vuex store action with CMS fetch"]
1030
+ });
1031
+ }
1032
+ if (commitCall) {
1033
+ candidates.push({
1034
+ filePath: relPath,
1035
+ line: commitCall.lineIndex + 1,
1036
+ type: "vuex-commit",
1037
+ score,
1038
+ confidence: scoreToConfidence(score),
1039
+ targetCode: commitCall.fullStatement,
1040
+ context: getContextLines(content, commitCall.lineIndex + 1, 8),
1041
+ reasons: [
1042
+ ...buildReasons(signals),
1043
+ ret ? "Vuex store action commits CMS data (also has return)" : "Vuex store action commits CMS data (no return)"
1044
+ ]
1045
+ });
1046
+ }
1047
+ if (!ret && !commitCall) continue;
1048
+ }
1049
+ }
1050
+ if ((isService || isApi) && hasCms) {
1051
+ candidates.push(
1052
+ ...findReturnStatements(content, filePath, cms, root, {
1053
+ isServerSide: isApi,
1054
+ isCentralLayer: isService
1055
+ })
1056
+ );
1057
+ }
1058
+ return candidates;
1059
+ }
1060
+ };
1061
+ var expressLikeStrategy = {
1062
+ globs: [
1063
+ "routes/**/*.ts",
1064
+ "routes/**/*.js",
1065
+ "src/routes/**/*.ts",
1066
+ "src/routes/**/*.js",
1067
+ "src/**/*.ts",
1068
+ "src/**/*.js",
1069
+ "api/**/*.ts",
1070
+ "api/**/*.js"
1071
+ ],
1072
+ analyze(filePath, content, cms, root) {
1073
+ const isRoute = /\broutes?\//.test(filePath) || /\bapi\//.test(filePath);
1074
+ return findReturnStatements(content, filePath, cms, root, {
1075
+ isServerSide: isRoute || /res\.json|res\.send|c\.json/.test(content),
1076
+ isCentralLayer: /\b(services?|helpers?|utils?|lib)\b/.test(filePath)
1077
+ });
1078
+ }
1079
+ };
1080
+ var STRATEGIES = {
1081
+ nuxt: nuxtStrategy,
1082
+ nuxt2: nuxt2Strategy,
1083
+ next: nextStrategy,
1084
+ remix: remixStrategy,
1085
+ astro: astroStrategy,
1086
+ sveltekit: sveltekitStrategy,
1087
+ express: expressLikeStrategy,
1088
+ hono: expressLikeStrategy,
1089
+ fastify: expressLikeStrategy
1090
+ };
1091
+ function computeAutoPatchConfidence(candidate) {
1092
+ if (candidate.score >= 85) return "high";
1093
+ if (candidate.score >= 65) return "medium";
1094
+ return "low";
1095
+ }
1096
+ function buildCandidateGraph(candidates) {
1097
+ const roots = /* @__PURE__ */ new Map();
1098
+ const regions = [];
1099
+ const graphCandidates = candidates.map((candidate, idx) => {
1100
+ const rootId = `${candidate.filePath}:root`;
1101
+ if (!roots.has(rootId)) {
1102
+ const rootRegion = {
1103
+ id: rootId,
1104
+ filePath: candidate.filePath,
1105
+ type: "unknown",
1106
+ startLine: 1,
1107
+ endLine: Number.MAX_SAFE_INTEGER,
1108
+ parentRegionId: null
1109
+ };
1110
+ roots.set(rootId, rootRegion);
1111
+ regions.push(rootRegion);
1112
+ }
1113
+ const regionId = `${candidate.filePath}:region:${idx + 1}`;
1114
+ regions.push({
1115
+ id: regionId,
1116
+ filePath: candidate.filePath,
1117
+ type: "unknown",
1118
+ startLine: Math.max(1, candidate.line - 2),
1119
+ endLine: candidate.line + 2,
1120
+ parentRegionId: rootId
1121
+ });
1122
+ return {
1123
+ ...candidate,
1124
+ detectedConfidence: candidate.detectedConfidence ?? candidate.confidence,
1125
+ autoPatchConfidence: candidate.autoPatchConfidence ?? computeAutoPatchConfidence(candidate),
1126
+ regionId
1127
+ };
1128
+ });
1129
+ return {
1130
+ regions,
1131
+ candidates: graphCandidates
1132
+ };
1133
+ }
1134
+ function findInjectionPoints(root, frameworkName, cms) {
1135
+ const strategy = STRATEGIES[frameworkName] ?? expressLikeStrategy;
1136
+ const files = fg.sync(strategy.globs, {
1137
+ cwd: root,
1138
+ ignore: IGNORE_DIRS.map((d) => `${d}/**`),
1139
+ absolute: true,
1140
+ onlyFiles: true,
1141
+ deep: 8
1142
+ });
1143
+ const allCandidates = [];
1144
+ for (const file of files) {
1145
+ let content;
1146
+ try {
1147
+ content = readFileSync(file, "utf-8");
1148
+ } catch {
1149
+ continue;
1150
+ }
1151
+ const candidates = strategy.analyze(file, content, cms, root);
1152
+ allCandidates.push(...candidates);
1153
+ }
1154
+ const seen = /* @__PURE__ */ new Set();
1155
+ const sortedCandidates = allCandidates.sort((a, b) => b.score - a.score).filter((c) => {
1156
+ const key = `${c.filePath}:${c.line}`;
1157
+ if (seen.has(key)) return false;
1158
+ seen.add(key);
1159
+ return true;
1160
+ }).map((candidate) => ({
1161
+ ...candidate,
1162
+ detectedConfidence: candidate.confidence,
1163
+ autoPatchConfidence: computeAutoPatchConfidence(candidate)
1164
+ }));
1165
+ const candidateGraph = buildCandidateGraph(sortedCandidates);
1166
+ return {
1167
+ candidates: candidateGraph.candidates,
1168
+ candidateGraph
1169
+ };
1170
+ }
1171
+
1172
+ export {
1173
+ findInjectionPoints,
1174
+ getTransformFunctionName,
1175
+ getImportStatement,
1176
+ buildCmsOptions,
1177
+ WRAP_TEMPLATES
1178
+ };