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