@synchronized-studio/cmsassets-agent 0.2.0 → 0.2.1

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,1073 @@
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 (isFactoryReturn(lines, i, trimmed)) continue;
342
+ if (/[;}]\s*$/.test(trimmed) || trimmed.includes("}") && trimmed.indexOf("}") > trimmed.indexOf("{")) {
343
+ return { lineIndex: i, trimmed };
344
+ }
345
+ let returnBrace = 0;
346
+ const parts = [];
347
+ for (let j = i; j < lines.length; j++) {
348
+ const ln = lines[j];
349
+ parts.push(ln);
350
+ for (const c of ln) {
351
+ if (c === "{") returnBrace++;
352
+ else if (c === "}") returnBrace--;
353
+ }
354
+ if (returnBrace === 0) {
355
+ return { lineIndex: i, trimmed: parts.join("\n").trim() };
356
+ }
357
+ }
358
+ return { lineIndex: i, trimmed };
359
+ }
360
+ if (braceDepth === 0 && i > startLineIndex) break;
361
+ }
362
+ return null;
363
+ }
364
+ function expandToFullCall(lines, startLineIndex) {
365
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
366
+ const firstLine = lines[startLineIndex];
367
+ const match = firstLine.match(pattern);
368
+ if (!match) return firstLine.trim();
369
+ const fnName = match[1];
370
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
371
+ if (openParenIdx === -1) return firstLine.trim();
372
+ let depth = 1;
373
+ const parts = [];
374
+ for (let i = startLineIndex; i < lines.length; i++) {
375
+ const line = lines[i];
376
+ parts.push(line);
377
+ const full = parts.join("\n");
378
+ const scanStart = i === startLineIndex ? openParenIdx + 1 : full.length - line.length;
379
+ for (let k = scanStart; k < full.length; k++) {
380
+ const c = full[k];
381
+ if (c === "(") depth++;
382
+ else if (c === ")") {
383
+ depth--;
384
+ if (depth === 0) return full.substring(0, k + 1);
385
+ }
386
+ }
387
+ }
388
+ return parts.join("\n").trim();
389
+ }
390
+ function findCallEndLine(lines, startLineIndex) {
391
+ const pattern = /\b(useAsyncData|useFetch)\s*\(/;
392
+ const firstLine = lines[startLineIndex];
393
+ const match = firstLine.match(pattern);
394
+ if (!match) return startLineIndex + 1;
395
+ const fnName = match[1];
396
+ const openParenIdx = firstLine.indexOf("(", firstLine.indexOf(fnName));
397
+ if (openParenIdx === -1) return startLineIndex + 1;
398
+ let depth = 1;
399
+ for (let i = startLineIndex; i < lines.length; i++) {
400
+ const line = lines[i];
401
+ const startCol = i === startLineIndex ? openParenIdx + 1 : 0;
402
+ for (let k = startCol; k < line.length; k++) {
403
+ const c = line[k];
404
+ if (c === "(") depth++;
405
+ else if (c === ")") {
406
+ depth--;
407
+ if (depth === 0) return i + 1;
408
+ }
409
+ }
410
+ }
411
+ return startLineIndex + 1;
412
+ }
413
+ var NON_DATA_RETURN_PATTERNS = [
414
+ /^return\s+(true|false|null|undefined|void)\b/,
415
+ /^return\s+["'`]/,
416
+ // return "string"
417
+ /^return\s+\d/,
418
+ // return 42
419
+ /^return\s+\w+\s*(===|!==|==|!=|>|<|>=|<=)/,
420
+ // return x === "true"
421
+ /^return\s+!\w/,
422
+ // return !value
423
+ /^return\s*$/,
424
+ // bare return
425
+ /^return\s*;?\s*$/,
426
+ // return;
427
+ /^return\s+err\b/,
428
+ // return err / error
429
+ /^return\s+error\b/,
430
+ /^return\s+\[\s*\]/,
431
+ // return []
432
+ /^return\s+\{\s*\}/,
433
+ // return {}
434
+ /^return\s+dispatch\s*\(/,
435
+ // return dispatch(...) -- Vuex forwarding
436
+ /^return\s+commit\s*\(/,
437
+ // return commit(...)
438
+ /^return\s+_get\s*\(/,
439
+ // return _get(...) -- lodash getter
440
+ /^return\s+_\w+\s*\(/,
441
+ // return _clone(...) etc -- lodash util
442
+ /^return\s+\w+\s*\?\s*/,
443
+ // return x ? y : z -- ternary
444
+ /^return\s+\w+\.\w+\s*(===|!==|==|!=)/,
445
+ // return link.link_type === "Web"
446
+ /^return\s+(axios|fetch|http|request)\b/,
447
+ // return axios / return fetch (the promise, not data)
448
+ /^return\s+res\.(json|send|status)\s*\(/,
449
+ // return res.json(...) -- handled separately as res.json type
450
+ /^return\s+"#"/
451
+ // return "#"
452
+ ];
453
+ var CALLBACK_PROPERTY_PATTERNS = [
454
+ /shouldBypassCache/,
455
+ /shouldInvalidateCache/,
456
+ /getKey/,
457
+ /onRequest/,
458
+ /onResponse/,
459
+ /onError/,
460
+ /transform\s*:/,
461
+ /validate\s*:/
462
+ ];
463
+ function isInsideCallbackOption(lines, returnLineIdx) {
464
+ for (let i = returnLineIdx - 1; i >= Math.max(0, returnLineIdx - 15); i--) {
465
+ const line = lines[i].trim();
466
+ if (CALLBACK_PROPERTY_PATTERNS.some((p) => p.test(line))) return true;
467
+ if (/^(export\s+default|async\s+function|function\s)/.test(line)) break;
468
+ if (/^(export\s+default\s+)?define\w+Handler/.test(line)) break;
469
+ }
470
+ return false;
471
+ }
472
+ function looksLikeDataReturn(trimmed) {
473
+ if (NON_DATA_RETURN_PATTERNS.some((p) => p.test(trimmed))) return false;
474
+ if (/^return\s+\{\s*\w/.test(trimmed)) return true;
475
+ if (/^return\s+\{\s*$/.test(trimmed)) return true;
476
+ if (/^return\s+\[\s*\w/.test(trimmed)) return true;
477
+ if (/^return\s+await\s+\w+\.\w+/.test(trimmed)) return true;
478
+ if (/^return\s+[a-zA-Z_]\w*\s*;?\s*$/.test(trimmed)) return true;
479
+ if (/\.push\s*\(|\.splice\s*\(|\.concat\s*\(/.test(trimmed)) return false;
480
+ if (/^return\s+Response\.json\s*\(/.test(trimmed)) return true;
481
+ if (/^return\s+json\s*\(/.test(trimmed)) return true;
482
+ if (/^return\s+new\s+Response/.test(trimmed)) return true;
483
+ return false;
484
+ }
485
+ function extractShorthandProps(returnCode) {
486
+ const match = returnCode.match(/^return\s+\{([^}]+)\}\s*;?\s*$/);
487
+ if (!match) return null;
488
+ const inner = match[1].trim();
489
+ const parts = inner.split(",").map((p) => p.trim()).filter(Boolean);
490
+ const names = [];
491
+ for (const part of parts) {
492
+ if (/^\w+$/.test(part)) {
493
+ names.push(part);
494
+ } else {
495
+ return null;
496
+ }
497
+ }
498
+ return names.length > 0 ? names : null;
499
+ }
500
+ var FUNCTION_DECLARATION_PATTERNS = [
501
+ /^(?:const|let)\s+NAME\s*=\s*cache\s*\(/,
502
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?\(/,
503
+ /^(?:const|let)\s+NAME\s*=\s*(?:async\s+)?(?:\w+\s*=>\s*|function)/,
504
+ /^(?:async\s+)?function\s+NAME\s*\(/
505
+ ];
506
+ function isFactoryReturn(lines, returnLineIdx, returnCode) {
507
+ const props = extractShorthandProps(returnCode);
508
+ if (!props || props.length === 0) return false;
509
+ const searchStart = Math.max(0, returnLineIdx - 200);
510
+ const scope = lines.slice(searchStart, returnLineIdx);
511
+ return props.every((name) => {
512
+ const patterns = FUNCTION_DECLARATION_PATTERNS.map(
513
+ (p) => new RegExp(p.source.replace("NAME", name))
514
+ );
515
+ return scope.some((line) => {
516
+ const trimmed = line.trim();
517
+ return patterns.some((p) => p.test(trimmed));
518
+ });
519
+ });
520
+ }
521
+ function findReturnStatements(content, filePath, cms, root, opts) {
522
+ const candidates = [];
523
+ const lines = content.split("\n");
524
+ const hasCms = hasCmsFetchSignal(content);
525
+ const relPath = relative(root, filePath);
526
+ const isTest = isTestFile(relPath);
527
+ for (let i = 0; i < lines.length; i++) {
528
+ const line = lines[i];
529
+ const trimmed = line.trim();
530
+ if (/^return\s/.test(trimmed)) {
531
+ if (!looksLikeDataReturn(trimmed)) continue;
532
+ if (isInsideCallbackOption(lines, i)) continue;
533
+ let fullReturn = trimmed;
534
+ if (/^return\s+\{\s*$/.test(trimmed)) {
535
+ let braceCount = 0;
536
+ const parts = [];
537
+ for (let j = i; j < lines.length; j++) {
538
+ parts.push(lines[j]);
539
+ for (const c of lines[j]) {
540
+ if (c === "{") braceCount++;
541
+ else if (c === "}") braceCount--;
542
+ }
543
+ if (braceCount === 0) break;
544
+ }
545
+ fullReturn = parts.join("\n").trim();
546
+ }
547
+ if (isFactoryReturn(lines, i, fullReturn)) continue;
548
+ if (/^return\s+\{/.test(fullReturn) && !hasCms && !opts.isServerSide) continue;
549
+ const signals = {
550
+ hasCmsFetch: hasCms,
551
+ isServerSide: opts.isServerSide,
552
+ returnsJson: true,
553
+ isCentralLayer: opts.isCentralLayer,
554
+ isTest
555
+ };
556
+ const score = computeScore(signals);
557
+ if (score < 50 || !signals.hasCmsFetch && !signals.isServerSide) {
558
+ continue;
559
+ }
560
+ candidates.push({
561
+ filePath: relative(root, filePath),
562
+ line: i + 1,
563
+ type: "return",
564
+ score,
565
+ confidence: scoreToConfidence(score),
566
+ targetCode: fullReturn,
567
+ context: getContextLines(content, i + 1),
568
+ reasons: buildReasons(signals)
569
+ });
570
+ }
571
+ if (/res\.json\s*\(/.test(trimmed)) {
572
+ if (isInsideCallbackOption(lines, i)) continue;
573
+ const signals = {
574
+ hasCmsFetch: hasCms,
575
+ isServerSide: true,
576
+ returnsJson: true,
577
+ isCentralLayer: opts.isCentralLayer,
578
+ isTest
579
+ };
580
+ const score = computeScore(signals);
581
+ if (score < 50) {
582
+ continue;
583
+ }
584
+ candidates.push({
585
+ filePath: relative(root, filePath),
586
+ line: i + 1,
587
+ type: "res.json",
588
+ score,
589
+ confidence: scoreToConfidence(score),
590
+ targetCode: trimmed,
591
+ context: getContextLines(content, i + 1),
592
+ reasons: buildReasons(signals)
593
+ });
594
+ }
595
+ }
596
+ return candidates;
597
+ }
598
+ var nuxtStrategy = {
599
+ globs: [
600
+ "server/api/**/*.ts",
601
+ "server/api/**/*.js",
602
+ "server/routes/**/*.ts",
603
+ "composables/**/*.ts",
604
+ "composables/**/*.js",
605
+ "pages/**/*.vue",
606
+ "pages/**/*.ts"
607
+ ],
608
+ analyze(filePath, content, cms, root) {
609
+ const relPath = relative(root, filePath);
610
+ const isServerApi = /server\/(api|routes)\//.test(filePath);
611
+ const isComposable = /composables\//.test(filePath);
612
+ const candidates = findReturnStatements(content, filePath, cms, root, {
613
+ isServerSide: isServerApi,
614
+ isCentralLayer: isComposable
615
+ });
616
+ const lines = content.split("\n");
617
+ const callbackReturnRanges = [];
618
+ for (let i = 0; i < lines.length; i++) {
619
+ const trimmed = lines[i].trim();
620
+ if (/\buseFetch\s*\(/.test(trimmed) && !/transform\s*:/.test(content.substring(content.indexOf(trimmed)))) {
621
+ const hasCms = hasCmsFetchSignal(content);
622
+ if (!hasCms) continue;
623
+ const signals = {
624
+ hasCmsFetch: hasCms,
625
+ isServerSide: false,
626
+ returnsJson: true,
627
+ isCentralLayer: isComposable,
628
+ isTest: isTestFile(relPath)
629
+ };
630
+ const score = computeScore(signals);
631
+ if (score < 50) continue;
632
+ candidates.push({
633
+ filePath: relPath,
634
+ line: i + 1,
635
+ type: "useFetch-transform",
636
+ score,
637
+ confidence: scoreToConfidence(score),
638
+ targetCode: expandToFullCall(lines, i),
639
+ context: getContextLines(content, i + 1),
640
+ reasons: [...buildReasons(signals), "useFetch call without transform option"]
641
+ });
642
+ }
643
+ if (/\buseAsyncData\s*\(/.test(trimmed)) {
644
+ const fullCall = expandToFullCall(lines, i);
645
+ if (/transform\s*:/.test(fullCall)) continue;
646
+ const hasCms = hasCmsFetchSignal(content);
647
+ if (!hasCms) continue;
648
+ callbackReturnRanges.push({
649
+ start: i + 1,
650
+ end: findCallEndLine(lines, i)
651
+ });
652
+ const signals = {
653
+ hasCmsFetch: hasCms,
654
+ isServerSide: false,
655
+ returnsJson: true,
656
+ isCentralLayer: isComposable,
657
+ isTest: isTestFile(relPath)
658
+ };
659
+ const score = computeScore(signals) + (/pages\//.test(filePath) ? 10 : 0);
660
+ if (score < 50) continue;
661
+ candidates.push({
662
+ filePath: relPath,
663
+ line: i + 1,
664
+ type: "useAsyncData-transform",
665
+ score: Math.min(100, score),
666
+ confidence: scoreToConfidence(score),
667
+ targetCode: fullCall,
668
+ context: getContextLines(content, i + 1),
669
+ reasons: [...buildReasons(signals), "useAsyncData call"]
670
+ });
671
+ }
672
+ }
673
+ if (callbackReturnRanges.length === 0) return candidates;
674
+ return candidates.filter((candidate) => {
675
+ if (candidate.type !== "return") return true;
676
+ return !callbackReturnRanges.some(
677
+ (range) => candidate.line > range.start && candidate.line <= range.end
678
+ );
679
+ });
680
+ }
681
+ };
682
+ var nextStrategy = {
683
+ globs: [
684
+ "app/**/page.tsx",
685
+ "app/**/page.ts",
686
+ "app/**/page.jsx",
687
+ "app/**/page.js",
688
+ "app/api/**/route.ts",
689
+ "app/api/**/route.js",
690
+ "pages/**/*.tsx",
691
+ "pages/**/*.ts",
692
+ "pages/**/*.jsx",
693
+ "pages/**/*.js",
694
+ "src/app/**/page.tsx",
695
+ "src/app/api/**/route.ts",
696
+ "src/pages/**/*.tsx",
697
+ "src/pages/**/*.ts",
698
+ "lib/**/*.ts",
699
+ "lib/**/*.js",
700
+ "src/lib/**/*.ts",
701
+ "src/lib/**/*.js"
702
+ ],
703
+ analyze(filePath, content, cms, root) {
704
+ const relPath = relative(root, filePath);
705
+ const isApiRoute = /\/api\//.test(filePath) && /route\.[jt]sx?$/.test(filePath);
706
+ const isAppRouterPage = /(^|\/)(src\/)?app\/(?:.*\/)?page\.[jt]sx?$/.test(relPath);
707
+ const isLib = /\blib\//.test(filePath);
708
+ const candidates = isAppRouterPage ? [] : findReturnStatements(content, filePath, cms, root, {
709
+ isServerSide: isApiRoute || /getServerSideProps|getStaticProps/.test(content),
710
+ isCentralLayer: isLib
711
+ });
712
+ const lines = content.split("\n");
713
+ for (let i = 0; i < lines.length; i++) {
714
+ const trimmed = lines[i].trim();
715
+ if (/export\s+(async\s+)?function\s+getServerSideProps/.test(trimmed)) {
716
+ const signals = {
717
+ hasCmsFetch: hasCmsFetchSignal(content),
718
+ isServerSide: true,
719
+ returnsJson: true,
720
+ isCentralLayer: false,
721
+ isTest: isTestFile(relPath)
722
+ };
723
+ const score = computeScore(signals);
724
+ candidates.push({
725
+ filePath: relPath,
726
+ line: i + 1,
727
+ type: "getServerSideProps-return",
728
+ score,
729
+ confidence: scoreToConfidence(score),
730
+ targetCode: trimmed,
731
+ context: getContextLines(content, i + 1),
732
+ reasons: buildReasons(signals)
733
+ });
734
+ }
735
+ if (/export\s+(async\s+)?function\s+getStaticProps/.test(trimmed)) {
736
+ const signals = {
737
+ hasCmsFetch: hasCmsFetchSignal(content),
738
+ isServerSide: true,
739
+ returnsJson: true,
740
+ isCentralLayer: false,
741
+ isTest: isTestFile(relPath)
742
+ };
743
+ const score = computeScore(signals);
744
+ candidates.push({
745
+ filePath: relPath,
746
+ line: i + 1,
747
+ type: "getStaticProps-return",
748
+ score,
749
+ confidence: scoreToConfidence(score),
750
+ targetCode: trimmed,
751
+ context: getContextLines(content, i + 1),
752
+ reasons: buildReasons(signals)
753
+ });
754
+ }
755
+ }
756
+ return candidates;
757
+ }
758
+ };
759
+ var remixStrategy = {
760
+ globs: ["app/routes/**/*.tsx", "app/routes/**/*.ts", "app/routes/**/*.jsx", "app/routes/**/*.js"],
761
+ analyze(filePath, content, cms, root) {
762
+ const relPath = relative(root, filePath);
763
+ const candidates = [];
764
+ const lines = content.split("\n");
765
+ for (let i = 0; i < lines.length; i++) {
766
+ const trimmed = lines[i].trim();
767
+ if (/export\s+(async\s+)?function\s+loader/.test(trimmed) || /export\s+const\s+loader/.test(trimmed)) {
768
+ const signals = {
769
+ hasCmsFetch: hasCmsFetchSignal(content),
770
+ isServerSide: true,
771
+ returnsJson: true,
772
+ isCentralLayer: false,
773
+ isTest: isTestFile(relPath)
774
+ };
775
+ const score = computeScore(signals);
776
+ candidates.push({
777
+ filePath: relPath,
778
+ line: i + 1,
779
+ type: "loader-return",
780
+ score,
781
+ confidence: scoreToConfidence(score),
782
+ targetCode: trimmed,
783
+ context: getContextLines(content, i + 1, 5),
784
+ reasons: [...buildReasons(signals), "Remix loader function"]
785
+ });
786
+ }
787
+ }
788
+ candidates.push(
789
+ ...findReturnStatements(content, filePath, cms, root, {
790
+ isServerSide: true,
791
+ isCentralLayer: false
792
+ })
793
+ );
794
+ return candidates;
795
+ }
796
+ };
797
+ var astroStrategy = {
798
+ globs: ["src/pages/**/*.astro", "src/pages/api/**/*.ts", "src/pages/api/**/*.js", "src/lib/**/*.ts"],
799
+ analyze(filePath, content, cms, root) {
800
+ const isApi = /\/api\//.test(filePath);
801
+ const isLib = /\blib\//.test(filePath);
802
+ if (filePath.endsWith(".astro")) {
803
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
804
+ if (frontmatterMatch) {
805
+ const frontmatter = frontmatterMatch[1];
806
+ if (hasCmsFetchSignal(frontmatter)) {
807
+ const assignmentPattern = /(?:const|let)\s+(\w+)\s*=\s*await\s+/g;
808
+ let m;
809
+ const candidates = [];
810
+ while ((m = assignmentPattern.exec(frontmatter)) !== null) {
811
+ const linesBefore = frontmatter.substring(0, m.index).split("\n").length;
812
+ const signals = {
813
+ hasCmsFetch: true,
814
+ isServerSide: true,
815
+ returnsJson: true,
816
+ isCentralLayer: false,
817
+ isTest: false
818
+ };
819
+ const score = computeScore(signals);
820
+ const matchedLine = frontmatter.split("\n")[linesBefore]?.trim() ?? m[0].trim();
821
+ candidates.push({
822
+ filePath: relative(root, filePath),
823
+ line: linesBefore + 1,
824
+ type: "frontmatter-assignment",
825
+ score,
826
+ confidence: scoreToConfidence(score),
827
+ targetCode: matchedLine,
828
+ context: getContextLines(frontmatter, linesBefore + 1),
829
+ reasons: [...buildReasons(signals), "Astro frontmatter data fetch"]
830
+ });
831
+ }
832
+ return candidates;
833
+ }
834
+ }
835
+ return [];
836
+ }
837
+ return findReturnStatements(content, filePath, cms, root, {
838
+ isServerSide: isApi,
839
+ isCentralLayer: isLib
840
+ });
841
+ }
842
+ };
843
+ var sveltekitStrategy = {
844
+ globs: [
845
+ "src/routes/**/+page.server.ts",
846
+ "src/routes/**/+page.server.js",
847
+ "src/routes/**/+server.ts",
848
+ "src/routes/**/+server.js",
849
+ "src/routes/**/+layout.server.ts",
850
+ "src/routes/**/+layout.server.js",
851
+ "src/lib/**/*.ts",
852
+ "src/lib/**/*.js"
853
+ ],
854
+ analyze(filePath, content, cms, root) {
855
+ const relPath = relative(root, filePath);
856
+ const isServer = /\+(?:page|layout)\.server\.[jt]s$/.test(filePath) || /\+server\.[jt]s$/.test(filePath);
857
+ const isLib = /src\/lib\//.test(filePath);
858
+ const candidates = [];
859
+ const lines = content.split("\n");
860
+ for (let i = 0; i < lines.length; i++) {
861
+ const trimmed = lines[i].trim();
862
+ if (/export\s+(async\s+)?function\s+load/.test(trimmed) || /export\s+const\s+load/.test(trimmed)) {
863
+ const signals = {
864
+ hasCmsFetch: hasCmsFetchSignal(content),
865
+ isServerSide: isServer,
866
+ returnsJson: true,
867
+ isCentralLayer: isLib,
868
+ isTest: isTestFile(relPath)
869
+ };
870
+ const score = computeScore(signals);
871
+ candidates.push({
872
+ filePath: relPath,
873
+ line: i + 1,
874
+ type: "load-return",
875
+ score,
876
+ confidence: scoreToConfidence(score),
877
+ targetCode: trimmed,
878
+ context: getContextLines(content, i + 1, 5),
879
+ reasons: [...buildReasons(signals), "SvelteKit load function"]
880
+ });
881
+ }
882
+ }
883
+ candidates.push(
884
+ ...findReturnStatements(content, filePath, cms, root, {
885
+ isServerSide: isServer,
886
+ isCentralLayer: isLib
887
+ })
888
+ );
889
+ return candidates;
890
+ }
891
+ };
892
+ var nuxt2Strategy = {
893
+ globs: [
894
+ "store/**/*.js",
895
+ "store/**/*.ts",
896
+ "pages/**/*.vue",
897
+ "mixins/**/*.js",
898
+ "mixins/**/*.ts",
899
+ "services/**/*.js",
900
+ "services/**/*.ts",
901
+ "api/**/*.js",
902
+ "api/**/*.ts",
903
+ "plugins/**/*.js",
904
+ "plugins/**/*.ts"
905
+ ],
906
+ analyze(filePath, content, cms, root) {
907
+ const relPath = relative(root, filePath);
908
+ const isStore = /store\//.test(filePath);
909
+ const isService = /services\//.test(filePath);
910
+ const isApi = /\bapi\//.test(filePath);
911
+ const isPage = /pages\//.test(filePath);
912
+ const candidates = [];
913
+ const hasCms = hasCmsFetchSignal(content);
914
+ const isTest = isTestFile(relPath);
915
+ const lines = content.split("\n");
916
+ if (isPage && filePath.endsWith(".vue")) {
917
+ for (let i = 0; i < lines.length; i++) {
918
+ const trimmed = lines[i].trim();
919
+ if (/async\s+asyncData\s*\(/.test(trimmed) || /asyncData\s*\(\s*\{/.test(trimmed)) {
920
+ const ret = findReturnLineInBody(lines, i);
921
+ if (!ret) continue;
922
+ const signals = {
923
+ hasCmsFetch: hasCms || /\$prismic|\$contentful|\$sanity/.test(content),
924
+ isServerSide: true,
925
+ returnsJson: true,
926
+ isCentralLayer: false,
927
+ isTest
928
+ };
929
+ const score = computeScore(signals);
930
+ if (score >= 50) {
931
+ candidates.push({
932
+ filePath: relPath,
933
+ line: ret.lineIndex + 1,
934
+ type: "asyncData-return",
935
+ score,
936
+ confidence: scoreToConfidence(score),
937
+ targetCode: ret.trimmed,
938
+ context: getContextLines(content, ret.lineIndex + 1, 5),
939
+ reasons: [...buildReasons(signals), "Nuxt 2 asyncData hook"]
940
+ });
941
+ }
942
+ }
943
+ }
944
+ }
945
+ if (isStore) {
946
+ const cmsCallPatterns = [
947
+ /this\.\$prismic/,
948
+ /this\.\$contentful/,
949
+ /this\.\$sanity/,
950
+ /client\.get/,
951
+ /client\.getSingle/,
952
+ /client\.getByUID/,
953
+ /client\.getAllByType/,
954
+ /client\.fetch/
955
+ ];
956
+ for (let i = 0; i < lines.length; i++) {
957
+ const trimmed = lines[i].trim();
958
+ if (!/^async\s+\w+\s*\(/.test(trimmed)) continue;
959
+ let bodyHasCmsCall = false;
960
+ for (let j = i + 1; j < Math.min(lines.length, i + 50); j++) {
961
+ const bodyLine = lines[j];
962
+ if (/^\s*(async\s+)?\w+\s*\(/.test(bodyLine) && j > i + 1 && !bodyLine.trim().startsWith("//")) {
963
+ if (/^\s{0,4}(async\s+)?\w+\s*\(\s*\{/.test(bodyLine)) break;
964
+ }
965
+ if (cmsCallPatterns.some((p) => p.test(bodyLine))) {
966
+ bodyHasCmsCall = true;
967
+ break;
968
+ }
969
+ }
970
+ if (!bodyHasCmsCall) continue;
971
+ const ret = findReturnLineInBody(lines, i);
972
+ if (!ret) continue;
973
+ const signals = {
974
+ hasCmsFetch: true,
975
+ isServerSide: true,
976
+ returnsJson: true,
977
+ isCentralLayer: true,
978
+ isTest
979
+ };
980
+ const score = computeScore(signals);
981
+ candidates.push({
982
+ filePath: relPath,
983
+ line: ret.lineIndex + 1,
984
+ type: "vuex-action-return",
985
+ score,
986
+ confidence: scoreToConfidence(score),
987
+ targetCode: ret.trimmed,
988
+ context: getContextLines(content, ret.lineIndex + 1, 8),
989
+ reasons: [...buildReasons(signals), "Vuex store action with CMS fetch"]
990
+ });
991
+ }
992
+ }
993
+ if ((isService || isApi) && hasCms) {
994
+ candidates.push(
995
+ ...findReturnStatements(content, filePath, cms, root, {
996
+ isServerSide: isApi,
997
+ isCentralLayer: isService
998
+ })
999
+ );
1000
+ }
1001
+ return candidates;
1002
+ }
1003
+ };
1004
+ var expressLikeStrategy = {
1005
+ globs: [
1006
+ "routes/**/*.ts",
1007
+ "routes/**/*.js",
1008
+ "src/routes/**/*.ts",
1009
+ "src/routes/**/*.js",
1010
+ "src/**/*.ts",
1011
+ "src/**/*.js",
1012
+ "api/**/*.ts",
1013
+ "api/**/*.js"
1014
+ ],
1015
+ analyze(filePath, content, cms, root) {
1016
+ const isRoute = /\broutes?\//.test(filePath) || /\bapi\//.test(filePath);
1017
+ return findReturnStatements(content, filePath, cms, root, {
1018
+ isServerSide: isRoute || /res\.json|res\.send|c\.json/.test(content),
1019
+ isCentralLayer: /\b(services?|helpers?|utils?|lib)\b/.test(filePath)
1020
+ });
1021
+ }
1022
+ };
1023
+ var STRATEGIES = {
1024
+ nuxt: nuxtStrategy,
1025
+ nuxt2: nuxt2Strategy,
1026
+ next: nextStrategy,
1027
+ remix: remixStrategy,
1028
+ astro: astroStrategy,
1029
+ sveltekit: sveltekitStrategy,
1030
+ express: expressLikeStrategy,
1031
+ hono: expressLikeStrategy,
1032
+ fastify: expressLikeStrategy
1033
+ };
1034
+ function findInjectionPoints(root, frameworkName, cms) {
1035
+ const strategy = STRATEGIES[frameworkName] ?? expressLikeStrategy;
1036
+ const files = fg.sync(strategy.globs, {
1037
+ cwd: root,
1038
+ ignore: IGNORE_DIRS.map((d) => `${d}/**`),
1039
+ absolute: true,
1040
+ onlyFiles: true,
1041
+ deep: 8
1042
+ });
1043
+ const allCandidates = [];
1044
+ for (const file of files) {
1045
+ let content;
1046
+ try {
1047
+ content = readFileSync(file, "utf-8");
1048
+ } catch {
1049
+ continue;
1050
+ }
1051
+ 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")) {
1052
+ continue;
1053
+ }
1054
+ const candidates = strategy.analyze(file, content, cms, root);
1055
+ allCandidates.push(...candidates);
1056
+ }
1057
+ const seen = /* @__PURE__ */ new Set();
1058
+ return allCandidates.sort((a, b) => b.score - a.score).filter((c) => {
1059
+ const key = `${c.filePath}:${c.line}`;
1060
+ if (seen.has(key)) return false;
1061
+ seen.add(key);
1062
+ return true;
1063
+ });
1064
+ }
1065
+
1066
+ export {
1067
+ findInjectionPoints,
1068
+ getTransformFunctionName,
1069
+ getImportStatement,
1070
+ buildCmsOptions,
1071
+ WRAP_TEMPLATES,
1072
+ validateDiff
1073
+ };