@synchronized-studio/cmsassets-agent 0.5.1 → 0.6.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,1224 @@
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
+ // Package imports
254
+ /from\s+['"]@prismicio\//,
255
+ /from\s+['"]@prismic\//,
256
+ /from\s+['"]contentful['"\/]/,
257
+ /from\s+['"]@contentful\//,
258
+ /from\s+['"]@sanity\//,
259
+ /from\s+['"]next-sanity['"\/]/,
260
+ /from\s+['"]@shopify\//,
261
+ /from\s+['"]@shopify\/hydrogen/,
262
+ /from\s+['"]cloudinary['"\/]/,
263
+ /from\s+['"]@cloudinary\//,
264
+ /from\s+['"]imgix['"\/]/,
265
+ /require\s*\(\s*['"]@prismicio\//,
266
+ /require\s*\(\s*['"]contentful/,
267
+ /require\s*\(\s*['"]@sanity\//,
268
+ // CMS SDK instance method calls (Nuxt inject / Vue context)
269
+ /\$prismic\.\w+\s*\(/,
270
+ /\$contentful\.\w+\s*\(/,
271
+ /\$sanity\.\w+\s*\(/,
272
+ // Common CMS composables / factory functions
273
+ /\busePrismic\s*\(/,
274
+ /\bcreateClient\s*\(\s*\)/,
275
+ // @prismicio/client pattern (no args — bare createClient())
276
+ /\bprismic\.client\b/,
277
+ /\bclient\.(getSingle|getByUID|getAllByType|get|fetch)\s*\(/,
278
+ // CMS CDN URL patterns (remain as-is — appear in fetch/axios calls after comment strip)
279
+ /cdn\.prismic\.io/i,
280
+ /cdn\.sanity\.io/i,
281
+ /ctfassets\.net/i,
282
+ /myshopify\.com/i,
283
+ /res\.cloudinary\.com/i,
284
+ /\.imgix\.net/i
285
+ ];
286
+ function hasCmsFetchSignal(content) {
287
+ const stripped = content.split("\n").map((line) => {
288
+ if (/^\s*(import\s|.*\brequire\s*\()/.test(line)) return line;
289
+ line = line.replace(/\/\/.*$/, "");
290
+ return line.replace(/(['"`])(?:(?!\1)[^\\]|\\.)*?\1/g, "$1$1");
291
+ }).join("\n").replace(/\/\*[\s\S]*?\*\//g, "");
292
+ return CMS_FETCH_PATTERNS.some((p) => p.test(stripped));
293
+ }
294
+ function isTestFile(filePath) {
295
+ return TEST_PATTERNS.some((p) => p.test(filePath));
296
+ }
297
+ function computeScore(signals) {
298
+ let score = 0;
299
+ if (signals.hasCmsFetch) score += 40;
300
+ if (signals.isServerSide) score += 30;
301
+ if (signals.returnsJson) score += 20;
302
+ if (signals.isCentralLayer) score += 10;
303
+ if (signals.isTest) score -= 20;
304
+ return Math.max(0, Math.min(100, score));
305
+ }
306
+ function scoreToConfidence(score) {
307
+ if (score >= 80) return "high";
308
+ if (score >= 60) return "medium";
309
+ return "low";
310
+ }
311
+ function buildReasons(signals) {
312
+ const reasons = [];
313
+ if (signals.hasCmsFetch) reasons.push("File contains CMS SDK import or fetch to CMS domain");
314
+ if (signals.isServerSide) reasons.push("Server-side file (API route, loader, SSR handler)");
315
+ if (signals.returnsJson) reasons.push("Returns JSON data (return statement / res.json)");
316
+ if (signals.isCentralLayer) reasons.push("Centralized data layer (composable, service, helper)");
317
+ if (signals.isTest) reasons.push("Test/fixture file (score reduced)");
318
+ return reasons;
319
+ }
320
+ function getContextLines(content, line, range = 2) {
321
+ const lines = content.split("\n");
322
+ const start = Math.max(0, line - 1 - range);
323
+ const end = Math.min(lines.length, line + range);
324
+ return lines.slice(start, end).join("\n");
325
+ }
326
+ function findReturnLineInBody(lines, startLineIndex) {
327
+ let braceDepth = 0;
328
+ for (let i = startLineIndex; i < lines.length; i++) {
329
+ const line = lines[i];
330
+ const trimmed = line.trim();
331
+ for (const c of line) {
332
+ if (c === "{") braceDepth++;
333
+ else if (c === "}") braceDepth--;
334
+ }
335
+ if (braceDepth > 0 && /^return\s/.test(trimmed) && looksLikeDataReturn(trimmed)) {
336
+ if (isFactoryReturn(lines, i, trimmed)) continue;
337
+ if (/[;}]\s*$/.test(trimmed) || trimmed.includes("}") && trimmed.indexOf("}") > trimmed.indexOf("{")) {
338
+ return { lineIndex: i, trimmed };
339
+ }
340
+ let returnBrace = 0;
341
+ const parts = [];
342
+ for (let j = i; j < lines.length; j++) {
343
+ const ln = lines[j];
344
+ parts.push(ln);
345
+ for (const c of ln) {
346
+ if (c === "{") returnBrace++;
347
+ else if (c === "}") returnBrace--;
348
+ }
349
+ if (returnBrace === 0) {
350
+ return { lineIndex: i, trimmed: parts.join("\n").trim() };
351
+ }
352
+ }
353
+ return { lineIndex: i, trimmed };
354
+ }
355
+ if (braceDepth === 0 && i > startLineIndex) break;
356
+ }
357
+ return null;
358
+ }
359
+ function expandToFullCall(lines, startLineIndex) {
360
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
361
+ const firstLine = lines[startLineIndex];
362
+ const match = firstLine.match(pattern);
363
+ if (!match) return firstLine.trim();
364
+ const fnName = match[1];
365
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
366
+ if (openParenIdx === -1) return firstLine.trim();
367
+ let depth = 1;
368
+ const parts = [];
369
+ for (let i = startLineIndex; i < lines.length; i++) {
370
+ const line = lines[i];
371
+ parts.push(line);
372
+ const full = parts.join("\n");
373
+ const scanStart = i === startLineIndex ? openParenIdx + 1 : full.length - line.length;
374
+ for (let k = scanStart; k < full.length; k++) {
375
+ const c = full[k];
376
+ if (c === "(") depth++;
377
+ else if (c === ")") {
378
+ depth--;
379
+ if (depth === 0) return full.substring(0, k + 1);
380
+ }
381
+ }
382
+ }
383
+ return parts.join("\n").trim();
384
+ }
385
+ function findCallEndLine(lines, startLineIndex) {
386
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
387
+ const firstLine = lines[startLineIndex];
388
+ const match = firstLine.match(pattern);
389
+ if (!match) return startLineIndex + 1;
390
+ const fnName = match[1];
391
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
392
+ if (openParenIdx === -1) return startLineIndex + 1;
393
+ let depth = 1;
394
+ for (let i = startLineIndex; i < lines.length; i++) {
395
+ const line = lines[i];
396
+ const startCol = i === startLineIndex ? openParenIdx + 1 : 0;
397
+ for (let k = startCol; k < line.length; k++) {
398
+ const c = line[k];
399
+ if (c === "(") depth++;
400
+ else if (c === ")") {
401
+ depth--;
402
+ if (depth === 0) return i + 1;
403
+ }
404
+ }
405
+ }
406
+ return startLineIndex + 1;
407
+ }
408
+ var NON_DATA_RETURN_PATTERNS = [
409
+ /^return\s+(true|false|null|undefined|void)\b/,
410
+ /^return\s+["'`]/,
411
+ // return "string"
412
+ /^return\s+\d/,
413
+ // return 42
414
+ /^return\s+\w+\s*(===|!==|==|!=|>|<|>=|<=)/,
415
+ // return x === "true"
416
+ /^return\s+!\w/,
417
+ // return !value
418
+ /^return\s*$/,
419
+ // bare return
420
+ /^return\s*;?\s*$/,
421
+ // return;
422
+ /^return\s+err\b/,
423
+ // return err / error
424
+ /^return\s+error\b/,
425
+ /^return\s+\[\s*\]/,
426
+ // return []
427
+ /^return\s+\{\s*\}/,
428
+ // return {}
429
+ /^return\s+dispatch\s*\(/,
430
+ // return dispatch(...) -- Vuex forwarding
431
+ /^return\s+commit\s*\(/,
432
+ // return commit(...)
433
+ /^return\s+(?:this\.\$store\.)?dispatch\s*\(/,
434
+ // return this.$store.dispatch(...)
435
+ /^return\s+store\.dispatch\s*\(/,
436
+ // return store.dispatch(...)
437
+ /^return\s+_get\s*\(/,
438
+ // return _get(...) -- lodash getter
439
+ /^return\s+_\w+\s*\(/,
440
+ // return _clone(...) etc -- lodash util
441
+ /^return\s+\w+\s*\?\s*/,
442
+ // return x ? y : z -- ternary
443
+ /^return\s+\w+\.\w+\s*(===|!==|==|!=)/,
444
+ // return link.link_type === "Web"
445
+ /^return\s+(axios|fetch|http|request)\b/,
446
+ // return axios / return fetch (the promise, not data)
447
+ /^return\s+res\.(json|send|status)\s*\(/,
448
+ // return res.json(...) -- handled separately as res.json type
449
+ /^return\s+"#"/,
450
+ // return "#"
451
+ /^return\s+\{\s*query\s*[,:}]/,
452
+ // return { query: ... } -- router/URL query object
453
+ /^return\s+\{\s*path\s*:/,
454
+ // return { path: "/..." } -- router navigation
455
+ /^return\s+\{\s*redirect\s*:/,
456
+ // return { redirect: ... } -- redirect object
457
+ /^return\s+\{\s*statusCode\s*:/,
458
+ // return { statusCode: 301, ... } -- HTTP redirect
459
+ /^return\s+\{\s*headers\s*:/,
460
+ // return { headers: ... } -- HTTP headers
461
+ /^return\s+\{\s*\.\.\.\w[\w.]*[Qq]uery\b/,
462
+ // return { ...route.query } -- spread of query object
463
+ /^return\s+\{\s*\.\.\.\w[\w.]*[Pp]arams\b/
464
+ // return { ...route.params } -- spread of params
465
+ ];
466
+ var CALLBACK_PROPERTY_PATTERNS = [
467
+ /shouldBypassCache/,
468
+ /shouldInvalidateCache/,
469
+ /getKey/,
470
+ /onRequest/,
471
+ /onResponse/,
472
+ /onError/,
473
+ /transform\s*:/,
474
+ /validate\s*:/,
475
+ /filter\s*:/,
476
+ // query-builder / collection filter callback
477
+ /sort\s*:/,
478
+ // query-builder sort callback
479
+ /where\s*:/,
480
+ // query-builder where callback
481
+ /orderBy\s*:/,
482
+ // query-builder orderBy
483
+ /select\s*:/
484
+ // query-builder field selector
485
+ ];
486
+ function isInsideCallbackOption(lines, returnLineIdx) {
487
+ for (let i = returnLineIdx - 1; i >= Math.max(0, returnLineIdx - 15); i--) {
488
+ const line = lines[i].trim();
489
+ if (CALLBACK_PROPERTY_PATTERNS.some((p) => p.test(line))) return true;
490
+ if (/^(export\s+default|async\s+function|function\s)/.test(line)) break;
491
+ if (/^(export\s+default\s+)?define\w+Handler/.test(line)) break;
492
+ }
493
+ return false;
494
+ }
495
+ function looksLikeDataReturn(trimmed) {
496
+ if (NON_DATA_RETURN_PATTERNS.some((p) => p.test(trimmed))) return false;
497
+ if (/^return\s+\{\s*\w/.test(trimmed)) return true;
498
+ if (/^return\s+\{\s*$/.test(trimmed)) return true;
499
+ if (/^return\s+\[\s*\w/.test(trimmed)) return true;
500
+ if (/^return\s+await\s+\w+\.\w+/.test(trimmed)) return true;
501
+ if (/^return\s+[a-zA-Z_]\w*\s*;?\s*$/.test(trimmed)) return true;
502
+ if (/^return\s+\(?[a-zA-Z_]\w*\)?\s+as\s+[\w<>{}\[\]|&.,\s]+;?\s*$/.test(trimmed)) return true;
503
+ if (/^return\s+\(\s*[a-zA-Z_]\w*\s+as\s+[\w<>{}\[\]|&.,\s]+\s*\)\s*;?\s*$/.test(trimmed)) return true;
504
+ if (/\.push\s*\(|\.splice\s*\(|\.concat\s*\(/.test(trimmed)) return false;
505
+ if (/^return\s+Response\.json\s*\(/.test(trimmed)) return true;
506
+ if (/^return\s+json\s*\(/.test(trimmed)) return true;
507
+ if (/^return\s+new\s+Response/.test(trimmed)) return true;
508
+ return false;
509
+ }
510
+ function extractShorthandProps(returnCode) {
511
+ const match = returnCode.match(/^return\s+\{([^}]+)\}\s*;?\s*$/);
512
+ if (!match) return null;
513
+ const inner = match[1].trim();
514
+ const parts = inner.split(",").map((p) => p.trim()).filter(Boolean);
515
+ const names = [];
516
+ for (const part of parts) {
517
+ if (/^\w+$/.test(part)) {
518
+ names.push(part);
519
+ } else {
520
+ return null;
521
+ }
522
+ }
523
+ return names.length > 0 ? names : null;
524
+ }
525
+ var FUNCTION_DECLARATION_PATTERNS = [
526
+ /^(?:const|let)\s+NAME\s*=\s*cache\s*\(/,
527
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?\(/,
528
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?(?:\w+\s*=>\s*|function)/,
529
+ /^(?:async\s+)?function\s+NAME\s*\(/,
530
+ // Vue reactive wrappers: computed/ref/reactive produce non-serializable objects
531
+ /^(?:const|let)\s+NAME\s*=\s*(?:computed|ref|reactive|shallowRef|shallowReactive|toRef|toRefs)\s*\(/
532
+ ];
533
+ function isFactoryReturn(lines, returnLineIdx, returnCode) {
534
+ const props = extractShorthandProps(returnCode);
535
+ if (!props || props.length === 0) return false;
536
+ const searchStart = Math.max(0, returnLineIdx - 200);
537
+ const scope = lines.slice(searchStart, returnLineIdx);
538
+ return props.every((name) => {
539
+ const patterns = FUNCTION_DECLARATION_PATTERNS.map(
540
+ (p) => new RegExp(p.source.replace("NAME", name))
541
+ );
542
+ return scope.some((line) => {
543
+ const trimmed = line.trim();
544
+ return patterns.some((p) => p.test(trimmed));
545
+ });
546
+ });
547
+ }
548
+ function findCommitInBody(lines, startLineIndex) {
549
+ let braceDepth = 0;
550
+ for (let i = startLineIndex; i < lines.length; i++) {
551
+ const line = lines[i];
552
+ const trimmed = line.trim();
553
+ for (const c of line) {
554
+ if (c === "{") braceDepth++;
555
+ else if (c === "}") braceDepth--;
556
+ }
557
+ if (braceDepth === 0 && i > startLineIndex) break;
558
+ if (braceDepth > 0 && /\bcommit\s*\(/.test(trimmed) && /\bvalue\s*:/.test(trimmed)) {
559
+ if (/[)]\s*$/.test(trimmed)) {
560
+ return { lineIndex: i, fullStatement: trimmed };
561
+ }
562
+ let parenDepth = 0;
563
+ const parts = [];
564
+ for (let j = i; j < lines.length; j++) {
565
+ parts.push(lines[j]);
566
+ for (const c of lines[j]) {
567
+ if (c === "(") parenDepth++;
568
+ else if (c === ")") parenDepth--;
569
+ }
570
+ if (parenDepth <= 0) {
571
+ return { lineIndex: i, fullStatement: parts.join("\n").trim() };
572
+ }
573
+ }
574
+ return { lineIndex: i, fullStatement: trimmed };
575
+ }
576
+ if (braceDepth > 0 && (/\bcommit\s*\(\s*$/.test(trimmed) || /\bcommit\s*\(\s*["']/.test(trimmed))) {
577
+ let parenDepth = 0;
578
+ const parts = [];
579
+ let hasValue = false;
580
+ for (let j = i; j < Math.min(lines.length, i + 20); j++) {
581
+ parts.push(lines[j]);
582
+ if (/\bvalue\s*:/.test(lines[j])) hasValue = true;
583
+ for (const c of lines[j]) {
584
+ if (c === "(") parenDepth++;
585
+ else if (c === ")") parenDepth--;
586
+ }
587
+ if (parenDepth <= 0) {
588
+ if (hasValue) {
589
+ return { lineIndex: i, fullStatement: parts.join("\n").trim() };
590
+ }
591
+ break;
592
+ }
593
+ }
594
+ }
595
+ }
596
+ return null;
597
+ }
598
+ function findReturnStatements(content, filePath, cms, root, opts) {
599
+ const candidates = [];
600
+ const lines = content.split("\n");
601
+ const hasCms = hasCmsFetchSignal(content);
602
+ const relPath = relative(root, filePath);
603
+ const isTest = isTestFile(relPath);
604
+ for (let i = 0; i < lines.length; i++) {
605
+ const line = lines[i];
606
+ const trimmed = line.trim();
607
+ if (/^return\s/.test(trimmed)) {
608
+ if (!looksLikeDataReturn(trimmed)) continue;
609
+ if (isInsideCallbackOption(lines, i)) continue;
610
+ let fullReturn = trimmed;
611
+ if (/^return\s+\{\s*$/.test(trimmed)) {
612
+ let braceCount = 0;
613
+ const parts = [];
614
+ for (let j = i; j < lines.length; j++) {
615
+ parts.push(lines[j]);
616
+ for (const c of lines[j]) {
617
+ if (c === "{") braceCount++;
618
+ else if (c === "}") braceCount--;
619
+ }
620
+ if (braceCount === 0) break;
621
+ }
622
+ fullReturn = parts.join("\n").trim();
623
+ }
624
+ if (isFactoryReturn(lines, i, fullReturn)) continue;
625
+ if (/^return\s+\{/.test(fullReturn) && !hasCms && !opts.isServerSide) continue;
626
+ const signals = {
627
+ hasCmsFetch: hasCms,
628
+ isServerSide: opts.isServerSide,
629
+ returnsJson: true,
630
+ isCentralLayer: opts.isCentralLayer,
631
+ isTest
632
+ };
633
+ const score = computeScore(signals);
634
+ if (score < 50 || !signals.hasCmsFetch && !signals.isServerSide) {
635
+ continue;
636
+ }
637
+ candidates.push({
638
+ filePath: relative(root, filePath),
639
+ line: i + 1,
640
+ type: "return",
641
+ score,
642
+ confidence: scoreToConfidence(score),
643
+ targetCode: fullReturn,
644
+ context: getContextLines(content, i + 1),
645
+ reasons: buildReasons(signals)
646
+ });
647
+ }
648
+ if (/res\.json\s*\(/.test(trimmed)) {
649
+ if (isInsideCallbackOption(lines, i)) continue;
650
+ const signals = {
651
+ hasCmsFetch: hasCms,
652
+ isServerSide: true,
653
+ returnsJson: true,
654
+ isCentralLayer: opts.isCentralLayer,
655
+ isTest
656
+ };
657
+ const score = computeScore(signals);
658
+ if (score < 50) {
659
+ continue;
660
+ }
661
+ candidates.push({
662
+ filePath: relative(root, filePath),
663
+ line: i + 1,
664
+ type: "res.json",
665
+ score,
666
+ confidence: scoreToConfidence(score),
667
+ targetCode: trimmed,
668
+ context: getContextLines(content, i + 1),
669
+ reasons: buildReasons(signals)
670
+ });
671
+ }
672
+ }
673
+ return candidates;
674
+ }
675
+ var nuxtStrategy = {
676
+ globs: [
677
+ "server/api/**/*.ts",
678
+ "server/api/**/*.js",
679
+ "server/routes/**/*.ts",
680
+ "composables/**/*.ts",
681
+ "composables/**/*.js",
682
+ "pages/**/*.vue",
683
+ "pages/**/*.ts"
684
+ ],
685
+ analyze(filePath, content, cms, root) {
686
+ const relPath = relative(root, filePath);
687
+ const isServerApi = /server\/(api|routes)\//.test(filePath);
688
+ const isComposable = /composables\//.test(filePath);
689
+ const candidates = findReturnStatements(content, filePath, cms, root, {
690
+ isServerSide: isServerApi,
691
+ isCentralLayer: isComposable
692
+ });
693
+ const lines = content.split("\n");
694
+ const callbackReturnRanges = [];
695
+ for (let i = 0; i < lines.length; i++) {
696
+ const trimmed = lines[i].trim();
697
+ if (/\buseFetch\s*\(/.test(trimmed) && !/transform\s*:/.test(content.substring(content.indexOf(trimmed)))) {
698
+ const hasCms = hasCmsFetchSignal(content);
699
+ if (!hasCms) continue;
700
+ const signals = {
701
+ hasCmsFetch: hasCms,
702
+ isServerSide: false,
703
+ returnsJson: true,
704
+ isCentralLayer: isComposable,
705
+ isTest: isTestFile(relPath)
706
+ };
707
+ const score = computeScore(signals);
708
+ if (score < 50) continue;
709
+ candidates.push({
710
+ filePath: relPath,
711
+ line: i + 1,
712
+ type: "useFetch-transform",
713
+ score,
714
+ confidence: scoreToConfidence(score),
715
+ targetCode: expandToFullCall(lines, i),
716
+ context: getContextLines(content, i + 1),
717
+ reasons: [...buildReasons(signals), "useFetch call without transform option"]
718
+ });
719
+ }
720
+ if (/\buseAsyncData\s*\(/.test(trimmed)) {
721
+ const fullCall = expandToFullCall(lines, i);
722
+ if (/transform\s*:/.test(fullCall)) continue;
723
+ const hasCms = hasCmsFetchSignal(content);
724
+ if (!hasCms) continue;
725
+ callbackReturnRanges.push({
726
+ start: i + 1,
727
+ end: findCallEndLine(lines, i)
728
+ });
729
+ const signals = {
730
+ hasCmsFetch: hasCms,
731
+ isServerSide: false,
732
+ returnsJson: true,
733
+ isCentralLayer: isComposable,
734
+ isTest: isTestFile(relPath)
735
+ };
736
+ const score = computeScore(signals) + (/pages\//.test(filePath) ? 10 : 0);
737
+ if (score < 50) continue;
738
+ candidates.push({
739
+ filePath: relPath,
740
+ line: i + 1,
741
+ type: "useAsyncData-transform",
742
+ score: Math.min(100, score),
743
+ confidence: scoreToConfidence(score),
744
+ targetCode: fullCall,
745
+ context: getContextLines(content, i + 1),
746
+ reasons: [...buildReasons(signals), "useAsyncData call"]
747
+ });
748
+ }
749
+ }
750
+ const filteredByCallback = callbackReturnRanges.length === 0 ? candidates : candidates.filter((candidate) => {
751
+ if (candidate.type !== "return") return true;
752
+ return !callbackReturnRanges.some(
753
+ (range) => candidate.line > range.start && candidate.line <= range.end
754
+ );
755
+ });
756
+ if (filePath.endsWith(".vue")) {
757
+ const hasDataTransformTarget = filteredByCallback.some(
758
+ (candidate) => candidate.type === "useAsyncData-transform" || candidate.type === "useFetch-transform"
759
+ );
760
+ if (hasDataTransformTarget) {
761
+ return filteredByCallback.filter((candidate) => candidate.type !== "return");
762
+ }
763
+ }
764
+ return filteredByCallback;
765
+ }
766
+ };
767
+ var nextStrategy = {
768
+ globs: [
769
+ "app/**/page.tsx",
770
+ "app/**/page.ts",
771
+ "app/**/page.jsx",
772
+ "app/**/page.js",
773
+ "app/api/**/route.ts",
774
+ "app/api/**/route.js",
775
+ "pages/**/*.tsx",
776
+ "pages/**/*.ts",
777
+ "pages/**/*.jsx",
778
+ "pages/**/*.js",
779
+ "src/app/**/page.tsx",
780
+ "src/app/api/**/route.ts",
781
+ "src/pages/**/*.tsx",
782
+ "src/pages/**/*.ts",
783
+ "lib/**/*.ts",
784
+ "lib/**/*.js",
785
+ "src/lib/**/*.ts",
786
+ "src/lib/**/*.js"
787
+ ],
788
+ analyze(filePath, content, cms, root) {
789
+ const relPath = relative(root, filePath);
790
+ const isApiRoute = /\/api\//.test(filePath) && /route\.[jt]sx?$/.test(filePath);
791
+ const isAppRouterPage = /(^|\/)(src\/)?app\/(?:.*\/)?page\.[jt]sx?$/.test(relPath);
792
+ const isLib = /\blib\//.test(filePath);
793
+ const candidates = isAppRouterPage ? [] : findReturnStatements(content, filePath, cms, root, {
794
+ isServerSide: isApiRoute || /getServerSideProps|getStaticProps/.test(content),
795
+ isCentralLayer: isLib
796
+ });
797
+ const lines = content.split("\n");
798
+ for (let i = 0; i < lines.length; i++) {
799
+ const trimmed = lines[i].trim();
800
+ if (/export\s+(async\s+)?function\s+getServerSideProps/.test(trimmed)) {
801
+ const signals = {
802
+ hasCmsFetch: hasCmsFetchSignal(content),
803
+ isServerSide: true,
804
+ returnsJson: true,
805
+ isCentralLayer: false,
806
+ isTest: isTestFile(relPath)
807
+ };
808
+ const score = computeScore(signals);
809
+ candidates.push({
810
+ filePath: relPath,
811
+ line: i + 1,
812
+ type: "getServerSideProps-return",
813
+ score,
814
+ confidence: scoreToConfidence(score),
815
+ targetCode: trimmed,
816
+ context: getContextLines(content, i + 1),
817
+ reasons: buildReasons(signals)
818
+ });
819
+ }
820
+ if (/export\s+(async\s+)?function\s+getStaticProps/.test(trimmed)) {
821
+ const signals = {
822
+ hasCmsFetch: hasCmsFetchSignal(content),
823
+ isServerSide: true,
824
+ returnsJson: true,
825
+ isCentralLayer: false,
826
+ isTest: isTestFile(relPath)
827
+ };
828
+ const score = computeScore(signals);
829
+ candidates.push({
830
+ filePath: relPath,
831
+ line: i + 1,
832
+ type: "getStaticProps-return",
833
+ score,
834
+ confidence: scoreToConfidence(score),
835
+ targetCode: trimmed,
836
+ context: getContextLines(content, i + 1),
837
+ reasons: buildReasons(signals)
838
+ });
839
+ }
840
+ }
841
+ return candidates;
842
+ }
843
+ };
844
+ var remixStrategy = {
845
+ globs: ["app/routes/**/*.tsx", "app/routes/**/*.ts", "app/routes/**/*.jsx", "app/routes/**/*.js"],
846
+ analyze(filePath, content, cms, root) {
847
+ const relPath = relative(root, filePath);
848
+ const candidates = [];
849
+ const lines = content.split("\n");
850
+ for (let i = 0; i < lines.length; i++) {
851
+ const trimmed = lines[i].trim();
852
+ if (/export\s+(async\s+)?function\s+loader/.test(trimmed) || /export\s+const\s+loader/.test(trimmed)) {
853
+ const signals = {
854
+ hasCmsFetch: hasCmsFetchSignal(content),
855
+ isServerSide: true,
856
+ returnsJson: true,
857
+ isCentralLayer: false,
858
+ isTest: isTestFile(relPath)
859
+ };
860
+ const score = computeScore(signals);
861
+ candidates.push({
862
+ filePath: relPath,
863
+ line: i + 1,
864
+ type: "loader-return",
865
+ score,
866
+ confidence: scoreToConfidence(score),
867
+ targetCode: trimmed,
868
+ context: getContextLines(content, i + 1, 5),
869
+ reasons: [...buildReasons(signals), "Remix loader function"]
870
+ });
871
+ }
872
+ }
873
+ candidates.push(
874
+ ...findReturnStatements(content, filePath, cms, root, {
875
+ isServerSide: true,
876
+ isCentralLayer: false
877
+ })
878
+ );
879
+ return candidates;
880
+ }
881
+ };
882
+ var astroStrategy = {
883
+ globs: ["src/pages/**/*.astro", "src/pages/api/**/*.ts", "src/pages/api/**/*.js", "src/lib/**/*.ts"],
884
+ analyze(filePath, content, cms, root) {
885
+ const isApi = /\/api\//.test(filePath);
886
+ const isLib = /\blib\//.test(filePath);
887
+ if (filePath.endsWith(".astro")) {
888
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
889
+ if (frontmatterMatch) {
890
+ const frontmatter = frontmatterMatch[1];
891
+ if (hasCmsFetchSignal(frontmatter)) {
892
+ const assignmentPattern = /(?:const|let)\s+(\w+)\s*=\s*await\s+/g;
893
+ let m;
894
+ const candidates = [];
895
+ while ((m = assignmentPattern.exec(frontmatter)) !== null) {
896
+ const linesBefore = frontmatter.substring(0, m.index).split("\n").length;
897
+ const signals = {
898
+ hasCmsFetch: true,
899
+ isServerSide: true,
900
+ returnsJson: true,
901
+ isCentralLayer: false,
902
+ isTest: false
903
+ };
904
+ const score = computeScore(signals);
905
+ const matchedLine = frontmatter.split("\n")[linesBefore]?.trim() ?? m[0].trim();
906
+ candidates.push({
907
+ filePath: relative(root, filePath),
908
+ line: linesBefore + 1,
909
+ type: "frontmatter-assignment",
910
+ score,
911
+ confidence: scoreToConfidence(score),
912
+ targetCode: matchedLine,
913
+ context: getContextLines(frontmatter, linesBefore + 1),
914
+ reasons: [...buildReasons(signals), "Astro frontmatter data fetch"]
915
+ });
916
+ }
917
+ return candidates;
918
+ }
919
+ }
920
+ return [];
921
+ }
922
+ return findReturnStatements(content, filePath, cms, root, {
923
+ isServerSide: isApi,
924
+ isCentralLayer: isLib
925
+ });
926
+ }
927
+ };
928
+ var sveltekitStrategy = {
929
+ globs: [
930
+ "src/routes/**/+page.server.ts",
931
+ "src/routes/**/+page.server.js",
932
+ "src/routes/**/+server.ts",
933
+ "src/routes/**/+server.js",
934
+ "src/routes/**/+layout.server.ts",
935
+ "src/routes/**/+layout.server.js",
936
+ "src/lib/**/*.ts",
937
+ "src/lib/**/*.js"
938
+ ],
939
+ analyze(filePath, content, cms, root) {
940
+ const relPath = relative(root, filePath);
941
+ const isServer = /\+(?:page|layout)\.server\.[jt]s$/.test(filePath) || /\+server\.[jt]s$/.test(filePath);
942
+ const isLib = /src\/lib\//.test(filePath);
943
+ const candidates = [];
944
+ const lines = content.split("\n");
945
+ for (let i = 0; i < lines.length; i++) {
946
+ const trimmed = lines[i].trim();
947
+ if (/export\s+(async\s+)?function\s+load/.test(trimmed) || /export\s+const\s+load/.test(trimmed)) {
948
+ const signals = {
949
+ hasCmsFetch: hasCmsFetchSignal(content),
950
+ isServerSide: isServer,
951
+ returnsJson: true,
952
+ isCentralLayer: isLib,
953
+ isTest: isTestFile(relPath)
954
+ };
955
+ const score = computeScore(signals);
956
+ candidates.push({
957
+ filePath: relPath,
958
+ line: i + 1,
959
+ type: "load-return",
960
+ score,
961
+ confidence: scoreToConfidence(score),
962
+ targetCode: trimmed,
963
+ context: getContextLines(content, i + 1, 5),
964
+ reasons: [...buildReasons(signals), "SvelteKit load function"]
965
+ });
966
+ }
967
+ }
968
+ candidates.push(
969
+ ...findReturnStatements(content, filePath, cms, root, {
970
+ isServerSide: isServer,
971
+ isCentralLayer: isLib
972
+ })
973
+ );
974
+ return candidates;
975
+ }
976
+ };
977
+ var nuxt2Strategy = {
978
+ globs: [
979
+ "store/**/*.js",
980
+ "store/**/*.ts",
981
+ "pages/**/*.vue",
982
+ "mixins/**/*.js",
983
+ "mixins/**/*.ts",
984
+ "services/**/*.js",
985
+ "services/**/*.ts",
986
+ "api/**/*.js",
987
+ "api/**/*.ts",
988
+ "plugins/**/*.js",
989
+ "plugins/**/*.ts"
990
+ ],
991
+ analyze(filePath, content, cms, root) {
992
+ const relPath = relative(root, filePath);
993
+ const isStore = /store\//.test(filePath);
994
+ const isService = /services\//.test(filePath);
995
+ const isApi = /\bapi\//.test(filePath);
996
+ const isPage = /pages\//.test(filePath);
997
+ const candidates = [];
998
+ const hasCms = hasCmsFetchSignal(content);
999
+ const isTest = isTestFile(relPath);
1000
+ const lines = content.split("\n");
1001
+ if (isPage && filePath.endsWith(".vue")) {
1002
+ for (let i = 0; i < lines.length; i++) {
1003
+ const trimmed = lines[i].trim();
1004
+ if (/async\s+asyncData\s*\(/.test(trimmed) || /asyncData\s*\(\s*\{/.test(trimmed)) {
1005
+ const ret = findReturnLineInBody(lines, i);
1006
+ if (!ret) continue;
1007
+ const signals = {
1008
+ hasCmsFetch: hasCms || /\$prismic|\$contentful|\$sanity/.test(content),
1009
+ isServerSide: true,
1010
+ returnsJson: true,
1011
+ isCentralLayer: false,
1012
+ isTest
1013
+ };
1014
+ const score = computeScore(signals);
1015
+ if (score >= 50) {
1016
+ candidates.push({
1017
+ filePath: relPath,
1018
+ line: ret.lineIndex + 1,
1019
+ type: "asyncData-return",
1020
+ score,
1021
+ confidence: scoreToConfidence(score),
1022
+ targetCode: ret.trimmed,
1023
+ context: getContextLines(content, ret.lineIndex + 1, 5),
1024
+ reasons: [...buildReasons(signals), "Nuxt 2 asyncData hook"]
1025
+ });
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+ if (isStore) {
1031
+ const cmsCallPatterns = [
1032
+ /this\.\$prismic/,
1033
+ /this\.\$contentful/,
1034
+ /this\.\$sanity/,
1035
+ /client\.get/,
1036
+ /client\.getSingle/,
1037
+ /client\.getByUID/,
1038
+ /client\.getAllByType/,
1039
+ /client\.fetch/
1040
+ ];
1041
+ for (let i = 0; i < lines.length; i++) {
1042
+ const trimmed = lines[i].trim();
1043
+ if (!/^async\s+\w+\s*\(/.test(trimmed)) continue;
1044
+ let bodyHasCmsCall = false;
1045
+ for (let j = i + 1; j < Math.min(lines.length, i + 50); j++) {
1046
+ const bodyLine = lines[j];
1047
+ if (/^\s*(async\s+)?\w+\s*\(/.test(bodyLine) && j > i + 1 && !bodyLine.trim().startsWith("//")) {
1048
+ if (/^\s{0,4}(async\s+)?\w+\s*\(\s*\{/.test(bodyLine)) break;
1049
+ }
1050
+ if (cmsCallPatterns.some((p) => p.test(bodyLine))) {
1051
+ bodyHasCmsCall = true;
1052
+ break;
1053
+ }
1054
+ }
1055
+ if (!bodyHasCmsCall) continue;
1056
+ const signals = {
1057
+ hasCmsFetch: true,
1058
+ isServerSide: true,
1059
+ returnsJson: true,
1060
+ isCentralLayer: true,
1061
+ isTest
1062
+ };
1063
+ const score = computeScore(signals);
1064
+ const ret = findReturnLineInBody(lines, i);
1065
+ const commitCall = findCommitInBody(lines, i);
1066
+ if (ret) {
1067
+ candidates.push({
1068
+ filePath: relPath,
1069
+ line: ret.lineIndex + 1,
1070
+ type: "vuex-action-return",
1071
+ score,
1072
+ confidence: scoreToConfidence(score),
1073
+ targetCode: ret.trimmed,
1074
+ context: getContextLines(content, ret.lineIndex + 1, 8),
1075
+ reasons: [...buildReasons(signals), "Vuex store action with CMS fetch"]
1076
+ });
1077
+ }
1078
+ if (commitCall) {
1079
+ candidates.push({
1080
+ filePath: relPath,
1081
+ line: commitCall.lineIndex + 1,
1082
+ type: "vuex-commit",
1083
+ score,
1084
+ confidence: scoreToConfidence(score),
1085
+ targetCode: commitCall.fullStatement,
1086
+ context: getContextLines(content, commitCall.lineIndex + 1, 8),
1087
+ reasons: [
1088
+ ...buildReasons(signals),
1089
+ ret ? "Vuex store action commits CMS data (also has return)" : "Vuex store action commits CMS data (no return)"
1090
+ ]
1091
+ });
1092
+ }
1093
+ if (!ret && !commitCall) continue;
1094
+ }
1095
+ }
1096
+ if ((isService || isApi) && hasCms) {
1097
+ candidates.push(
1098
+ ...findReturnStatements(content, filePath, cms, root, {
1099
+ isServerSide: isApi,
1100
+ isCentralLayer: isService
1101
+ })
1102
+ );
1103
+ }
1104
+ return candidates;
1105
+ }
1106
+ };
1107
+ var expressLikeStrategy = {
1108
+ globs: [
1109
+ "routes/**/*.ts",
1110
+ "routes/**/*.js",
1111
+ "src/routes/**/*.ts",
1112
+ "src/routes/**/*.js",
1113
+ "src/**/*.ts",
1114
+ "src/**/*.js",
1115
+ "api/**/*.ts",
1116
+ "api/**/*.js"
1117
+ ],
1118
+ analyze(filePath, content, cms, root) {
1119
+ const isRoute = /\broutes?\//.test(filePath) || /\bapi\//.test(filePath);
1120
+ return findReturnStatements(content, filePath, cms, root, {
1121
+ isServerSide: isRoute || /res\.json|res\.send|c\.json/.test(content),
1122
+ isCentralLayer: /\b(services?|helpers?|utils?|lib)\b/.test(filePath)
1123
+ });
1124
+ }
1125
+ };
1126
+ var STRATEGIES = {
1127
+ nuxt: nuxtStrategy,
1128
+ nuxt2: nuxt2Strategy,
1129
+ next: nextStrategy,
1130
+ remix: remixStrategy,
1131
+ astro: astroStrategy,
1132
+ sveltekit: sveltekitStrategy,
1133
+ express: expressLikeStrategy,
1134
+ hono: expressLikeStrategy,
1135
+ fastify: expressLikeStrategy
1136
+ };
1137
+ function computeAutoPatchConfidence(candidate) {
1138
+ if (candidate.score >= 85) return "high";
1139
+ if (candidate.score >= 65) return "medium";
1140
+ return "low";
1141
+ }
1142
+ function buildCandidateGraph(candidates) {
1143
+ const roots = /* @__PURE__ */ new Map();
1144
+ const regions = [];
1145
+ const graphCandidates = candidates.map((candidate, idx) => {
1146
+ const rootId = `${candidate.filePath}:root`;
1147
+ if (!roots.has(rootId)) {
1148
+ const rootRegion = {
1149
+ id: rootId,
1150
+ filePath: candidate.filePath,
1151
+ type: "unknown",
1152
+ startLine: 1,
1153
+ endLine: Number.MAX_SAFE_INTEGER,
1154
+ parentRegionId: null
1155
+ };
1156
+ roots.set(rootId, rootRegion);
1157
+ regions.push(rootRegion);
1158
+ }
1159
+ const regionId = `${candidate.filePath}:region:${idx + 1}`;
1160
+ regions.push({
1161
+ id: regionId,
1162
+ filePath: candidate.filePath,
1163
+ type: "unknown",
1164
+ startLine: Math.max(1, candidate.line - 2),
1165
+ endLine: candidate.line + 2,
1166
+ parentRegionId: rootId
1167
+ });
1168
+ return {
1169
+ ...candidate,
1170
+ detectedConfidence: candidate.detectedConfidence ?? candidate.confidence,
1171
+ autoPatchConfidence: candidate.autoPatchConfidence ?? computeAutoPatchConfidence(candidate),
1172
+ regionId
1173
+ };
1174
+ });
1175
+ return {
1176
+ regions,
1177
+ candidates: graphCandidates
1178
+ };
1179
+ }
1180
+ function findInjectionPoints(root, frameworkName, cms) {
1181
+ const strategy = STRATEGIES[frameworkName] ?? expressLikeStrategy;
1182
+ const files = fg.sync(strategy.globs, {
1183
+ cwd: root,
1184
+ ignore: IGNORE_DIRS.map((d) => `${d}/**`),
1185
+ absolute: true,
1186
+ onlyFiles: true,
1187
+ deep: 8
1188
+ });
1189
+ const allCandidates = [];
1190
+ for (const file of files) {
1191
+ let content;
1192
+ try {
1193
+ content = readFileSync(file, "utf-8");
1194
+ } catch {
1195
+ continue;
1196
+ }
1197
+ const candidates = strategy.analyze(file, content, cms, root);
1198
+ allCandidates.push(...candidates);
1199
+ }
1200
+ const seen = /* @__PURE__ */ new Set();
1201
+ const sortedCandidates = allCandidates.sort((a, b) => b.score - a.score).filter((c) => {
1202
+ const key = `${c.filePath}:${c.line}`;
1203
+ if (seen.has(key)) return false;
1204
+ seen.add(key);
1205
+ return true;
1206
+ }).map((candidate) => ({
1207
+ ...candidate,
1208
+ detectedConfidence: candidate.confidence,
1209
+ autoPatchConfidence: computeAutoPatchConfidence(candidate)
1210
+ }));
1211
+ const candidateGraph = buildCandidateGraph(sortedCandidates);
1212
+ return {
1213
+ candidates: candidateGraph.candidates,
1214
+ candidateGraph
1215
+ };
1216
+ }
1217
+
1218
+ export {
1219
+ findInjectionPoints,
1220
+ getTransformFunctionName,
1221
+ getImportStatement,
1222
+ buildCmsOptions,
1223
+ WRAP_TEMPLATES
1224
+ };