mcp-server-sfmc 0.4.3 → 1.0.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.
- package/README.md +140 -31
- package/bundled/mcn-help/chunks.json +1 -0
- package/ci-templates/github-copilot-review-instructions.md +1 -1
- package/dist/conversion-rules.d.ts +149 -0
- package/dist/conversion-rules.d.ts.map +1 -0
- package/dist/conversion-rules.js +845 -0
- package/dist/conversion-rules.js.map +1 -0
- package/dist/index.js +832 -64
- package/dist/index.js.map +1 -1
- package/dist/mcn-help-search.d.ts +39 -0
- package/dist/mcn-help-search.d.ts.map +1 -0
- package/dist/mcn-help-search.js +88 -0
- package/dist/mcn-help-search.js.map +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared conversion rules for SSJS ↔ AMPscript and MCN rewriting.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for mapping tables and deterministic transformation
|
|
5
|
+
* logic used by:
|
|
6
|
+
* - rewrite_for_mcn tool
|
|
7
|
+
* - convertSsjsToAmpscript tool
|
|
8
|
+
* - convertAmpscriptToSsjs tool
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Platform.Function.X → AMPscript function name (lowercase key)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Maps a Platform.Function.X name (lowercase) to the equivalent AMPscript
|
|
15
|
+
* canonical function name. Only functions with a direct 1:1 equivalent are
|
|
16
|
+
* included.
|
|
17
|
+
*/
|
|
18
|
+
export const PLATFORM_FUNCTION_TO_AMP = {
|
|
19
|
+
lookup: 'Lookup',
|
|
20
|
+
lookuprows: 'LookupRows',
|
|
21
|
+
lookuporderedrows: 'LookupOrderedRows',
|
|
22
|
+
lookuporderedrowscs: 'LookupOrderedRowsCS',
|
|
23
|
+
insertde: 'InsertDE',
|
|
24
|
+
updatede: 'UpdateDE',
|
|
25
|
+
upsertde: 'UpsertDE',
|
|
26
|
+
deletede: 'DeleteDE',
|
|
27
|
+
rowcount: 'RowCount',
|
|
28
|
+
contentblockbyid: 'ContentBlockById',
|
|
29
|
+
contentblockbyname: 'ContentBlockByName',
|
|
30
|
+
contentblockbykey: 'ContentBlockByKey',
|
|
31
|
+
now: 'Now',
|
|
32
|
+
dateadd: 'DateAdd',
|
|
33
|
+
datediff: 'DateDiff',
|
|
34
|
+
dateparse: 'DateParse',
|
|
35
|
+
formatdate: 'FormatDate',
|
|
36
|
+
stringtodate: 'StringToDate',
|
|
37
|
+
concat: 'Concat',
|
|
38
|
+
substring: 'Substring',
|
|
39
|
+
trim: 'Trim',
|
|
40
|
+
lowercase: 'Lowercase',
|
|
41
|
+
uppercase: 'Uppercase',
|
|
42
|
+
propercase: 'ProperCase',
|
|
43
|
+
replace: 'Replace',
|
|
44
|
+
replacelist: 'ReplaceList',
|
|
45
|
+
indexof: 'IndexOf',
|
|
46
|
+
length: 'Length',
|
|
47
|
+
add: 'Add',
|
|
48
|
+
subtract: 'Subtract',
|
|
49
|
+
multiply: 'Multiply',
|
|
50
|
+
divide: 'Divide',
|
|
51
|
+
mod: 'Mod',
|
|
52
|
+
iif: 'Iif',
|
|
53
|
+
empty: 'Empty',
|
|
54
|
+
isnull: 'IsNull',
|
|
55
|
+
format: 'Format',
|
|
56
|
+
formatcurrency: 'FormatCurrency',
|
|
57
|
+
formatnumber: 'FormatNumber',
|
|
58
|
+
random: 'Random',
|
|
59
|
+
guid: 'GUID',
|
|
60
|
+
v: 'v',
|
|
61
|
+
output: 'Output',
|
|
62
|
+
outputline: 'OutputLine',
|
|
63
|
+
raiseerror: 'RaiseError',
|
|
64
|
+
httpget: 'HTTPGet',
|
|
65
|
+
httppost: 'HTTPPost',
|
|
66
|
+
httpgetwithcacheability: 'HTTPGetWithCacheability',
|
|
67
|
+
};
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// AMPscript function name → Platform.Function.X SSJS name (lowercase key)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Maps an AMPscript function name (lowercase) to its SSJS Platform.Function
|
|
73
|
+
* equivalent name (the part after "Platform.Function.").
|
|
74
|
+
*/
|
|
75
|
+
export const AMP_TO_PLATFORM_FUNCTION = {
|
|
76
|
+
lookup: 'Lookup',
|
|
77
|
+
lookuprows: 'LookupRows',
|
|
78
|
+
lookuporderedrows: 'LookupOrderedRows',
|
|
79
|
+
lookuporderedrowscs: 'LookupOrderedRowsCS',
|
|
80
|
+
insertde: 'InsertDE',
|
|
81
|
+
updatede: 'UpdateDE',
|
|
82
|
+
upsertde: 'UpsertDE',
|
|
83
|
+
deletede: 'DeleteDE',
|
|
84
|
+
rowcount: 'RowCount',
|
|
85
|
+
contentblockbyid: 'ContentBlockById',
|
|
86
|
+
contentblockbyname: 'ContentBlockByName',
|
|
87
|
+
contentblockbykey: 'ContentBlockByKey',
|
|
88
|
+
now: 'Now',
|
|
89
|
+
dateadd: 'DateAdd',
|
|
90
|
+
datediff: 'DateDiff',
|
|
91
|
+
dateparse: 'DateParse',
|
|
92
|
+
formatdate: 'FormatDate',
|
|
93
|
+
stringtodate: 'StringToDate',
|
|
94
|
+
concat: 'Concat',
|
|
95
|
+
substring: 'Substring',
|
|
96
|
+
trim: 'Trim',
|
|
97
|
+
lowercase: 'Lowercase',
|
|
98
|
+
uppercase: 'Uppercase',
|
|
99
|
+
propercase: 'ProperCase',
|
|
100
|
+
replace: 'Replace',
|
|
101
|
+
replacelist: 'ReplaceList',
|
|
102
|
+
indexof: 'IndexOf',
|
|
103
|
+
length: 'Length',
|
|
104
|
+
add: 'Add',
|
|
105
|
+
subtract: 'Subtract',
|
|
106
|
+
multiply: 'Multiply',
|
|
107
|
+
divide: 'Divide',
|
|
108
|
+
mod: 'Mod',
|
|
109
|
+
iif: 'Iif',
|
|
110
|
+
empty: 'Empty',
|
|
111
|
+
isnull: 'IsNull',
|
|
112
|
+
format: 'Format',
|
|
113
|
+
formatcurrency: 'FormatCurrency',
|
|
114
|
+
formatnumber: 'FormatNumber',
|
|
115
|
+
random: 'Random',
|
|
116
|
+
guid: 'GUID',
|
|
117
|
+
};
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// .NET → Java SimpleDateFormat format string replacements
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* Ordered list of [RegExp, replacement] pairs to convert .NET format specifiers
|
|
123
|
+
* in FormatDate() calls to Java SimpleDateFormat equivalents.
|
|
124
|
+
* Applied sequentially so more specific patterns match before general ones.
|
|
125
|
+
*/
|
|
126
|
+
export const DOTNET_TO_JAVA_FORMAT_REPLACEMENTS = [
|
|
127
|
+
// AM/PM marker: .NET 'tt' → Java 'a'
|
|
128
|
+
[/\btt\b/g, 'a'],
|
|
129
|
+
];
|
|
130
|
+
/**
|
|
131
|
+
* Set of .NET standard format shorthands that have no direct Java equivalent
|
|
132
|
+
* and must be replaced with an explicit pattern.
|
|
133
|
+
*/
|
|
134
|
+
export const DOTNET_STANDARD_SHORTHANDS = new Set([
|
|
135
|
+
'G',
|
|
136
|
+
'g',
|
|
137
|
+
'D',
|
|
138
|
+
'F',
|
|
139
|
+
'f',
|
|
140
|
+
'T',
|
|
141
|
+
't',
|
|
142
|
+
'R',
|
|
143
|
+
'r',
|
|
144
|
+
'U',
|
|
145
|
+
'u',
|
|
146
|
+
's',
|
|
147
|
+
'o',
|
|
148
|
+
'O',
|
|
149
|
+
]);
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// CloudPages-specific AMPscript functions (not available in MCN)
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Set of AMPscript function names (lowercase) that are available only in
|
|
155
|
+
* CloudPages / web content context and are not supported in Marketing Cloud Next.
|
|
156
|
+
*/
|
|
157
|
+
export const CLOUDPAGES_ONLY_FUNCTIONS = new Set([
|
|
158
|
+
'cloudpagesurl',
|
|
159
|
+
'requestparameter',
|
|
160
|
+
'queryparameter',
|
|
161
|
+
'redirect',
|
|
162
|
+
'micrositeurl',
|
|
163
|
+
'isprimarycontext',
|
|
164
|
+
'cloudpageurl',
|
|
165
|
+
'ampscriptnow',
|
|
166
|
+
]);
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Non-migratable SSJS patterns
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Patterns in SSJS code that indicate constructs with no AMPscript equivalent.
|
|
172
|
+
* Any SSJS block containing these patterns is classified as "Not migratable"
|
|
173
|
+
* in check_mcn_compatibility and marked MANUAL_REWRITE_REQUIRED in rewriting tools.
|
|
174
|
+
*/
|
|
175
|
+
export const NON_MIGRATABLE_SSJS_PATTERNS = [
|
|
176
|
+
{ pattern: /\btry\s*\{/, reason: 'try/catch has no AMPscript equivalent' },
|
|
177
|
+
{ pattern: /\bcatch\s*\(/, reason: 'try/catch has no AMPscript equivalent' },
|
|
178
|
+
{
|
|
179
|
+
pattern: /\bfinally\s*\{/,
|
|
180
|
+
reason: 'try/catch/finally has no AMPscript equivalent',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
pattern: /\.forEach\s*\(/,
|
|
184
|
+
reason: 'Array.forEach() has no AMPscript equivalent',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
pattern: /\.map\s*\(/,
|
|
188
|
+
reason: 'Array.map() has no AMPscript equivalent',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
pattern: /\.filter\s*\(/,
|
|
192
|
+
reason: 'Array.filter() has no AMPscript equivalent',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
pattern: /\.reduce\s*\(/,
|
|
196
|
+
reason: 'Array.reduce() has no AMPscript equivalent',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
pattern: /JSON\.stringify\s*\(/,
|
|
200
|
+
reason: 'JSON.stringify() has no AMPscript equivalent',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
pattern: /new\s+RegExp\s*\(/,
|
|
204
|
+
reason: 'Regular expressions have no AMPscript equivalent',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
pattern: /Platform\.Request\.(GetQueryStringParameter|GetFormField)\s*\(/,
|
|
208
|
+
reason: 'Query string / form field access requires CloudPages context (not available in MCN)',
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// SSJS → AMPscript conversion
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* Convert a SSJS code block to equivalent AMPscript using deterministic rules.
|
|
216
|
+
*
|
|
217
|
+
* Handles:
|
|
218
|
+
* - `Platform.Function.X(args)` → `X(args)`
|
|
219
|
+
* - `Platform.Variable.GetValue("name")` → `\@name`
|
|
220
|
+
* - `Platform.Variable.SetValue("name", val)` → `SET \@name = val`
|
|
221
|
+
* - `Platform.Response.Write(expr)` → `OutputLine(expr)`
|
|
222
|
+
* - `var x = expr;` → `SET \@x = expr`
|
|
223
|
+
* - `var x;` → `VAR \@x`
|
|
224
|
+
* - Control flow: if/else if/else/} → IF/ELSEIF/ELSE/ENDIF
|
|
225
|
+
* - `for (var i = start; i <= end; i++) {` → `FOR \@i = start TO end DO`
|
|
226
|
+
*
|
|
227
|
+
* Flags non-migratable constructs as MANUAL_REWRITE_REQUIRED.
|
|
228
|
+
* @param code - SSJS source code (may include `<script runat="server">` tags).
|
|
229
|
+
* @returns {ConversionResult} Conversion result with converted code, change log, and flagged sections.
|
|
230
|
+
*/
|
|
231
|
+
export function ssjsToAmpscript(code) {
|
|
232
|
+
const changes = [];
|
|
233
|
+
const flaggedSections = [];
|
|
234
|
+
// Strip <script runat="server"> wrappers if present
|
|
235
|
+
const inner = code
|
|
236
|
+
.replaceAll(/<script[^>]+runat=['"]?server['"]?[^>]*>/gi, '')
|
|
237
|
+
.replaceAll(/<\/script>/gi, '')
|
|
238
|
+
.trim();
|
|
239
|
+
const rawLines = inner.split('\n');
|
|
240
|
+
const outputLines = [];
|
|
241
|
+
for (const [i, original] of rawLines.entries()) {
|
|
242
|
+
const lineNum = i + 1;
|
|
243
|
+
const trimmed = original.trim();
|
|
244
|
+
// Skip blank lines, Platform.Load(), var-only declarations with no value
|
|
245
|
+
if (!trimmed) {
|
|
246
|
+
outputLines.push('');
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
// Skip Platform.Load() — no AMPscript equivalent, not needed in MCN
|
|
250
|
+
if (/^Platform\.Load\s*\(/i.test(trimmed)) {
|
|
251
|
+
changes.push({
|
|
252
|
+
line: lineNum,
|
|
253
|
+
description: 'Removed Platform.Load() (not needed in AMPscript)',
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Check for non-migratable patterns first
|
|
258
|
+
let isFlagged = false;
|
|
259
|
+
for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
|
|
260
|
+
// Reset lastIndex for global regexes
|
|
261
|
+
pattern.lastIndex = 0;
|
|
262
|
+
if (pattern.test(trimmed)) {
|
|
263
|
+
outputLines.push(`%%-- MANUAL_REWRITE_REQUIRED: ${reason} --%%`, `%%-- Original: ${trimmed} --%%`);
|
|
264
|
+
flaggedSections.push({ line: lineNum, code: trimmed, reason });
|
|
265
|
+
isFlagged = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (isFlagged) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
let line = original;
|
|
273
|
+
// Platform.Variable.GetValue("name") → @name
|
|
274
|
+
line = line.replaceAll(/Platform\.Variable\.GetValue\s*\(\s*["']([^"']+)["']\s*\)/gi, '@$1');
|
|
275
|
+
// Platform.Variable.SetValue("name", value) → SET @name = value (strip ; at end if present)
|
|
276
|
+
line = line.replaceAll(/Platform\.Variable\.SetValue\s*\(\s*["']([^"']+)["']\s*,\s*([^)]+)\)\s*;?/gi, (_, varName, value) => {
|
|
277
|
+
changes.push({
|
|
278
|
+
line: lineNum,
|
|
279
|
+
description: `Platform.Variable.SetValue → SET @${varName}`,
|
|
280
|
+
});
|
|
281
|
+
return `SET @${varName} = ${value.trim()}`;
|
|
282
|
+
});
|
|
283
|
+
// Platform.Response.Write(expr) → OutputLine(expr)
|
|
284
|
+
line = line.replaceAll(/Platform\.Response\.Write\s*\(/gi, () => {
|
|
285
|
+
changes.push({ line: lineNum, description: 'Platform.Response.Write → OutputLine' });
|
|
286
|
+
return 'OutputLine(';
|
|
287
|
+
});
|
|
288
|
+
// Platform.Function.X(args) → X(args) using known function map
|
|
289
|
+
line = line.replaceAll(/Platform\.Function\.(\w+)\s*\(/gi, (_, fnName) => {
|
|
290
|
+
const ampName = PLATFORM_FUNCTION_TO_AMP[fnName.toLowerCase()] ?? fnName;
|
|
291
|
+
changes.push({
|
|
292
|
+
line: lineNum,
|
|
293
|
+
description: `Platform.Function.${fnName} → ${ampName}`,
|
|
294
|
+
});
|
|
295
|
+
return `${ampName}(`;
|
|
296
|
+
});
|
|
297
|
+
// var x = expr; → SET @x = expr
|
|
298
|
+
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*=\s*(.+?)\s*;?\s*$/, (_, varName, value) => {
|
|
299
|
+
changes.push({
|
|
300
|
+
line: lineNum,
|
|
301
|
+
description: `var ${varName} = ... → SET @${varName}`,
|
|
302
|
+
});
|
|
303
|
+
return `SET @${varName} = ${value.trim()}`;
|
|
304
|
+
});
|
|
305
|
+
// var x; → VAR @x
|
|
306
|
+
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*;?\s*$/, (_, varName) => {
|
|
307
|
+
changes.push({ line: lineNum, description: `var ${varName} → VAR @${varName}` });
|
|
308
|
+
return `VAR @${varName}`;
|
|
309
|
+
});
|
|
310
|
+
// Control flow: if (cond) { → IF cond THEN
|
|
311
|
+
line = line.replace(/^\s*if\s*\((.+)\)\s*\{\s*$/, (_, cond) => {
|
|
312
|
+
const ampCond = ssjsCondToAmp(cond.trim());
|
|
313
|
+
changes.push({ line: lineNum, description: 'if (...) { → IF ... THEN' });
|
|
314
|
+
return `IF ${ampCond} THEN`;
|
|
315
|
+
});
|
|
316
|
+
// } else if (cond) { → ELSEIF cond THEN
|
|
317
|
+
line = line.replace(/^\s*\}\s*else\s+if\s*\((.+)\)\s*\{\s*$/, (_, cond) => {
|
|
318
|
+
const ampCond = ssjsCondToAmp(cond.trim());
|
|
319
|
+
changes.push({ line: lineNum, description: '} else if (...) { → ELSEIF ... THEN' });
|
|
320
|
+
return `ELSEIF ${ampCond} THEN`;
|
|
321
|
+
});
|
|
322
|
+
// } else { → ELSE
|
|
323
|
+
line = line.replace(/^\s*\}\s*else\s*\{\s*$/, () => {
|
|
324
|
+
changes.push({ line: lineNum, description: '} else { → ELSE' });
|
|
325
|
+
return 'ELSE';
|
|
326
|
+
});
|
|
327
|
+
// for (var i = start; i <= end; i++) { → FOR @i = start TO end DO
|
|
328
|
+
const forMatch = /^\s*for\s*\(\s*var\s+(\w+)\s*=\s*(\S+?)\s*;\s*\w+\s*<=?\s*(\S+?)\s*;\s*\w+\+\+\s*\)\s*\{\s*$/.exec(line);
|
|
329
|
+
if (forMatch) {
|
|
330
|
+
const [, iterVar, start, end] = forMatch;
|
|
331
|
+
changes.push({
|
|
332
|
+
line: lineNum,
|
|
333
|
+
description: `for (var ${iterVar}...) → FOR @${iterVar} = ${start} TO ${end} DO`,
|
|
334
|
+
});
|
|
335
|
+
line = `FOR @${iterVar} = ${start} TO ${end} DO`;
|
|
336
|
+
}
|
|
337
|
+
// Standalone closing brace } → ENDIF (best-effort; may not always be correct)
|
|
338
|
+
if (/^\s*\}\s*$/.test(line) && !/^\s*\}\s*(else|catch|finally)/.test(line)) {
|
|
339
|
+
changes.push({ line: lineNum, description: '} → ENDIF' });
|
|
340
|
+
line = 'ENDIF';
|
|
341
|
+
}
|
|
342
|
+
// Strip trailing semicolons from non-SET lines (AMPscript doesn't use them)
|
|
343
|
+
if (!/^\s*SET /i.test(line.trim()) && /;\s*$/.test(line)) {
|
|
344
|
+
line = line.replace(/;\s*$/, '');
|
|
345
|
+
}
|
|
346
|
+
outputLines.push(line.trim());
|
|
347
|
+
}
|
|
348
|
+
// Wrap in AMPscript block
|
|
349
|
+
const convertedCode = `%%[\n${outputLines.filter((l) => l !== undefined).join('\n')}\n]%%`;
|
|
350
|
+
return { convertedCode, changes, flaggedSections };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Convert simple SSJS conditional expression to AMPscript syntax.
|
|
354
|
+
* @param cond - JS condition string.
|
|
355
|
+
* @returns {string} AMPscript condition string.
|
|
356
|
+
*/
|
|
357
|
+
function ssjsCondToAmp(cond) {
|
|
358
|
+
// @variable references from previous conversions stay as-is
|
|
359
|
+
// Convert JS == / === to AMPscript == and != / !== to !=
|
|
360
|
+
return cond
|
|
361
|
+
.replaceAll(/!==\s*/g, '!= ')
|
|
362
|
+
.replaceAll(/===\s*/g, '== ')
|
|
363
|
+
.replaceAll('||', 'OR')
|
|
364
|
+
.replaceAll('&&', 'AND')
|
|
365
|
+
.replaceAll('!', 'NOT ');
|
|
366
|
+
}
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// AMPscript → SSJS conversion
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
/**
|
|
371
|
+
* Convert AMPscript code to equivalent SSJS using deterministic rules.
|
|
372
|
+
*
|
|
373
|
+
* Handles:
|
|
374
|
+
* - `%%[ SET \@x = expr ]%%` → `var x = expr;`
|
|
375
|
+
* - `%%[ VAR \@x, \@y ]%%` → `var x, y;`
|
|
376
|
+
* - `%%[ IF cond THEN / ELSEIF / ELSE / ENDIF ]%%` → JS control flow
|
|
377
|
+
* - `%%[ FOR \@i = start TO end DO / NEXT \@i ]%%` → for loop
|
|
378
|
+
* - `%%=Output(\@x)=%%` / `%%=OutputLine(\@x)=%%` → `Platform.Response.Write(x)`
|
|
379
|
+
* - `%%=FunctionName(args)=%%` → `Platform.Response.Write(Platform.Function.FunctionName(args))`
|
|
380
|
+
* - Known AMPscript functions → Platform.Function.X equivalents
|
|
381
|
+
* - `\@variable` references → bare variable names
|
|
382
|
+
* @param code - AMPscript source code.
|
|
383
|
+
* @returns {ConversionResult} Conversion result with converted SSJS, change log, and flagged sections.
|
|
384
|
+
*/
|
|
385
|
+
export function ampscriptToSsjs(code) {
|
|
386
|
+
const changes = [];
|
|
387
|
+
const flaggedSections = [];
|
|
388
|
+
const outputLines = ['<script runat="server">', 'Platform.Load("Core", "1.1.5");'];
|
|
389
|
+
// Normalize: combine multi-line %%[ ... ]%% blocks into single pseudo-lines
|
|
390
|
+
// then process line by line
|
|
391
|
+
const normalized = normalizeAmpscriptBlocks(code);
|
|
392
|
+
const lines = normalized.split('\n');
|
|
393
|
+
const lineOffset = 0;
|
|
394
|
+
for (const [i, line] of lines.entries()) {
|
|
395
|
+
const lineNum = i + 1 + lineOffset;
|
|
396
|
+
const trimmed = line.trim();
|
|
397
|
+
if (!trimmed) {
|
|
398
|
+
outputLines.push('');
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
// %%=Output(@x)=%% or %%=OutputLine(@x)=%%
|
|
402
|
+
const inlineOutputMatch = /^%%=\s*(?:Output|OutputLine)\s*\((.+)\)\s*=%%$/i.exec(trimmed);
|
|
403
|
+
if (inlineOutputMatch) {
|
|
404
|
+
const expr = stripAmpVars(inlineOutputMatch[1].trim());
|
|
405
|
+
changes.push({
|
|
406
|
+
line: lineNum,
|
|
407
|
+
description: '%%=Output(...)=%% → Platform.Response.Write(...)',
|
|
408
|
+
});
|
|
409
|
+
outputLines.push(`Platform.Response.Write(${expr});`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// %%=FunctionName(args)=%% → Platform.Response.Write(Platform.Function.FunctionName(args))
|
|
413
|
+
const inlineFnMatch = /^%%=\s*(\w+)\s*\((.*)?\)\s*=%%$/i.exec(trimmed);
|
|
414
|
+
if (inlineFnMatch) {
|
|
415
|
+
const fnName = inlineFnMatch[1];
|
|
416
|
+
const args = inlineFnMatch[2]?.trim() ?? '';
|
|
417
|
+
const ssName = AMP_TO_PLATFORM_FUNCTION[fnName.toLowerCase()];
|
|
418
|
+
const argsConverted = stripAmpVars(args);
|
|
419
|
+
if (ssName) {
|
|
420
|
+
changes.push({
|
|
421
|
+
line: lineNum,
|
|
422
|
+
description: `%%=${fnName}(...)=%% → Platform.Response.Write(Platform.Function.${ssName}(...))`,
|
|
423
|
+
});
|
|
424
|
+
outputLines.push(`Platform.Response.Write(Platform.Function.${ssName}(${argsConverted}));`);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Unknown function — pass through as comment for AI
|
|
428
|
+
outputLines.push(`/* MANUAL_REWRITE_REQUIRED: %%=${fnName}(${args})=%% */`);
|
|
429
|
+
flaggedSections.push({
|
|
430
|
+
line: lineNum,
|
|
431
|
+
code: trimmed,
|
|
432
|
+
reason: `Unknown AMPscript function '${fnName}' — no SSJS equivalent in catalog`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
// %%[ block content ]%%
|
|
438
|
+
const blockMatch = /^%%\[\s*([\s\S]*?)\s*\]%%$/i.exec(trimmed);
|
|
439
|
+
if (blockMatch) {
|
|
440
|
+
const blockContent = blockMatch[1].trim();
|
|
441
|
+
const stmts = blockContent.split(/\n+/);
|
|
442
|
+
for (const stmt of stmts) {
|
|
443
|
+
const converted = convertAmpStatement(stmt.trim(), lineNum, changes, flaggedSections);
|
|
444
|
+
if (converted !== null) {
|
|
445
|
+
outputLines.push(converted);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Bare AMPscript statement (already stripped of delimiters from normalizeAmpscriptBlocks)
|
|
451
|
+
if (/^(SET|VAR|IF|ELSEIF|ELSE|ENDIF|FOR|NEXT|OUTPUT|OUTPUTLINE)\b/i.test(trimmed)) {
|
|
452
|
+
const converted = convertAmpStatement(trimmed, lineNum, changes, flaggedSections);
|
|
453
|
+
if (converted !== null) {
|
|
454
|
+
outputLines.push(converted);
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Pass through non-AMPscript content as an HTML comment
|
|
459
|
+
if (trimmed) {
|
|
460
|
+
outputLines.push(`/* HTML content: ${trimmed.slice(0, 80)} */`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
outputLines.push('</script>');
|
|
464
|
+
return {
|
|
465
|
+
convertedCode: outputLines.join('\n'),
|
|
466
|
+
changes,
|
|
467
|
+
flaggedSections,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Normalize AMPscript block delimiters so each AMPscript statement is on its
|
|
472
|
+
* own line, with `%%[` and `]%%` stripped.
|
|
473
|
+
* @param code - Raw AMPscript source.
|
|
474
|
+
* @returns {string} Normalized code with one statement per line.
|
|
475
|
+
*/
|
|
476
|
+
function normalizeAmpscriptBlocks(code) {
|
|
477
|
+
// Expand %%[ ... ]%% to individual statements
|
|
478
|
+
let result = code;
|
|
479
|
+
result = result.replaceAll(/%%\[\s*([\s\S]*?)\s*\]%%/g, (_, inner) => inner
|
|
480
|
+
.split('\n')
|
|
481
|
+
.map((l) => l.trim())
|
|
482
|
+
.filter(Boolean)
|
|
483
|
+
.join('\n'));
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Convert a single AMPscript statement to SSJS.
|
|
488
|
+
* @param stmt - AMPscript statement string (no delimiters).
|
|
489
|
+
* @param lineNum - Source line number for change tracking.
|
|
490
|
+
* @param changes - Mutable changes array.
|
|
491
|
+
* @param flaggedSections - Mutable flagged sections array.
|
|
492
|
+
* @returns {string | null} SSJS statement string, or null to skip.
|
|
493
|
+
*/
|
|
494
|
+
function convertAmpStatement(stmt, lineNum, changes, flaggedSections) {
|
|
495
|
+
if (!stmt)
|
|
496
|
+
return null;
|
|
497
|
+
const upper = stmt.toUpperCase();
|
|
498
|
+
// SET @x = expr → var x = expr;
|
|
499
|
+
const setMatch = /^SET\s+@(\w+)\s*=\s*(.+)$/i.exec(stmt);
|
|
500
|
+
if (setMatch) {
|
|
501
|
+
const [, varName, expr] = setMatch;
|
|
502
|
+
const ssExpr = convertAmpExpr(expr.trim());
|
|
503
|
+
changes.push({
|
|
504
|
+
line: lineNum,
|
|
505
|
+
description: `SET @${varName} = ... → var ${varName} = ...`,
|
|
506
|
+
});
|
|
507
|
+
return `var ${varName} = ${ssExpr};`;
|
|
508
|
+
}
|
|
509
|
+
// VAR @x, @y → var x, y;
|
|
510
|
+
const varMatch = /^VAR\s+(.+)$/i.exec(stmt);
|
|
511
|
+
if (varMatch) {
|
|
512
|
+
const vars = varMatch[1].split(',').map((v) => v.trim().replace(/^@/, ''));
|
|
513
|
+
changes.push({
|
|
514
|
+
line: lineNum,
|
|
515
|
+
description: `VAR @${vars.join(', @')} → var ${vars.join(', ')}`,
|
|
516
|
+
});
|
|
517
|
+
return `var ${vars.join(', ')};`;
|
|
518
|
+
}
|
|
519
|
+
// IF cond THEN → if (cond) {
|
|
520
|
+
const ifMatch = /^IF\s+(.+?)\s+THEN$/i.exec(stmt);
|
|
521
|
+
if (ifMatch) {
|
|
522
|
+
const cond = ampCondToSsjs(ifMatch[1].trim());
|
|
523
|
+
changes.push({ line: lineNum, description: 'IF ... THEN → if (...) {' });
|
|
524
|
+
return `if (${cond}) {`;
|
|
525
|
+
}
|
|
526
|
+
// ELSEIF cond THEN → } else if (cond) {
|
|
527
|
+
const elseifMatch = /^ELSEIF\s+(.+?)\s+THEN$/i.exec(stmt);
|
|
528
|
+
if (elseifMatch) {
|
|
529
|
+
const cond = ampCondToSsjs(elseifMatch[1].trim());
|
|
530
|
+
changes.push({ line: lineNum, description: 'ELSEIF ... THEN → } else if (...) {' });
|
|
531
|
+
return `} else if (${cond}) {`;
|
|
532
|
+
}
|
|
533
|
+
// ELSE → } else {
|
|
534
|
+
if (/^ELSE$/i.test(stmt)) {
|
|
535
|
+
changes.push({ line: lineNum, description: 'ELSE → } else {' });
|
|
536
|
+
return '} else {';
|
|
537
|
+
}
|
|
538
|
+
// ENDIF → }
|
|
539
|
+
if (/^ENDIF$/i.test(stmt)) {
|
|
540
|
+
changes.push({ line: lineNum, description: 'ENDIF → }' });
|
|
541
|
+
return '}';
|
|
542
|
+
}
|
|
543
|
+
// FOR @i = start TO end DO → for (var i = start; i <= end; i++) {
|
|
544
|
+
const forMatch = /^FOR\s+@(\w+)\s*=\s*(\S+?)\s+TO\s+(\S+?)(?:\s+STEP\s+\S+)?\s+DO$/i.exec(stmt);
|
|
545
|
+
if (forMatch) {
|
|
546
|
+
const [, iterVar, start, end] = forMatch;
|
|
547
|
+
const ssStart = stripAmpVars(start);
|
|
548
|
+
const ssEnd = stripAmpVars(end);
|
|
549
|
+
changes.push({
|
|
550
|
+
line: lineNum,
|
|
551
|
+
description: `FOR @${iterVar} = ${start} TO ${end} DO → for loop`,
|
|
552
|
+
});
|
|
553
|
+
return `for (var ${iterVar} = ${ssStart}; ${iterVar} <= ${ssEnd}; ${iterVar}++) {`;
|
|
554
|
+
}
|
|
555
|
+
// NEXT @i → }
|
|
556
|
+
if (/^NEXT\s+@\w+$/i.test(stmt)) {
|
|
557
|
+
changes.push({ line: lineNum, description: 'NEXT @i → }' });
|
|
558
|
+
return '}';
|
|
559
|
+
}
|
|
560
|
+
// OUTPUT(expr) / OUTPUTLINE(expr) → Platform.Response.Write(expr)
|
|
561
|
+
const outputMatch = /^(?:OUTPUT|OUTPUTLINE)\s*\((.+)\)$/i.exec(stmt);
|
|
562
|
+
if (outputMatch) {
|
|
563
|
+
const expr = convertAmpExpr(outputMatch[1].trim());
|
|
564
|
+
changes.push({ line: lineNum, description: 'Output/OutputLine → Platform.Response.Write' });
|
|
565
|
+
return `Platform.Response.Write(${expr});`;
|
|
566
|
+
}
|
|
567
|
+
// Known AMPscript function call → Platform.Function.X(args)
|
|
568
|
+
const fnCallMatch = /^(\w+)\s*\((.*)?\)$/i.exec(stmt);
|
|
569
|
+
if (fnCallMatch) {
|
|
570
|
+
const [, fnName, args] = fnCallMatch;
|
|
571
|
+
const ssName = AMP_TO_PLATFORM_FUNCTION[fnName.toLowerCase()];
|
|
572
|
+
if (ssName) {
|
|
573
|
+
const argsConverted = args ? convertAmpExpr(args.trim()) : '';
|
|
574
|
+
changes.push({
|
|
575
|
+
line: lineNum,
|
|
576
|
+
description: `${fnName}(…) → Platform.Function.${ssName}(…)`,
|
|
577
|
+
});
|
|
578
|
+
return `Platform.Function.${ssName}(${argsConverted});`;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Check for AMPscript-only constructs that can't be converted
|
|
582
|
+
if (/\bCLOUDPAGESURL\b|\bREQUESTPARAMETER\b|\bQUERYPARAMETER\b|\bREDIRECT\b/i.test(stmt)) {
|
|
583
|
+
flaggedSections.push({
|
|
584
|
+
line: lineNum,
|
|
585
|
+
code: stmt,
|
|
586
|
+
reason: 'CloudPages-specific function — not available in SSJS/MCN context',
|
|
587
|
+
});
|
|
588
|
+
return `/* MANUAL_REWRITE_REQUIRED: ${stmt} */`;
|
|
589
|
+
}
|
|
590
|
+
// Unknown statement — pass through as comment
|
|
591
|
+
if (upper !== stmt.toUpperCase() || /[A-Za-z]/.test(stmt)) {
|
|
592
|
+
// Has alphabetic content — flag it
|
|
593
|
+
flaggedSections.push({
|
|
594
|
+
line: lineNum,
|
|
595
|
+
code: stmt,
|
|
596
|
+
reason: 'Could not automatically convert this AMPscript statement',
|
|
597
|
+
});
|
|
598
|
+
return `/* MANUAL_REWRITE_REQUIRED: ${stmt} */`;
|
|
599
|
+
}
|
|
600
|
+
return stmt;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Convert an AMPscript expression to its SSJS equivalent.
|
|
604
|
+
* Strips `@` from variable references and maps known function names.
|
|
605
|
+
* @param expr - AMPscript expression string.
|
|
606
|
+
* @returns {string} SSJS expression string.
|
|
607
|
+
*/
|
|
608
|
+
function convertAmpExpr(expr) {
|
|
609
|
+
// Replace known AMPscript function calls with Platform.Function.X equivalents
|
|
610
|
+
let result = expr.replaceAll(/\b(\w+)\s*\(/g, (match, fnName) => {
|
|
611
|
+
const ssName = AMP_TO_PLATFORM_FUNCTION[fnName.toLowerCase()];
|
|
612
|
+
return ssName ? `Platform.Function.${ssName}(` : match;
|
|
613
|
+
});
|
|
614
|
+
// Strip @ from variable references
|
|
615
|
+
result = stripAmpVars(result);
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Convert an AMPscript condition expression to SSJS syntax.
|
|
620
|
+
* @param cond - AMPscript condition string.
|
|
621
|
+
* @returns {string} SSJS condition string.
|
|
622
|
+
*/
|
|
623
|
+
function ampCondToSsjs(cond) {
|
|
624
|
+
return stripAmpVars(cond)
|
|
625
|
+
.replaceAll(/\bAND\b/gi, '&&')
|
|
626
|
+
.replaceAll(/\bOR\b/gi, '||')
|
|
627
|
+
.replaceAll(/\bNOT\b/gi, '!')
|
|
628
|
+
.replaceAll(/\bEQUAL\s+TO\b/gi, '===')
|
|
629
|
+
.replaceAll(/\bNOT\s+EQUAL\s+TO\b/gi, '!==')
|
|
630
|
+
.replaceAll(/\bGREATER\s+THAN\b/gi, '>')
|
|
631
|
+
.replaceAll(/\bLESS\s+THAN\b/gi, '<');
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Strip `@` prefix from AMPscript variable references in an expression.
|
|
635
|
+
* @param expr - Expression possibly containing `@varName` references.
|
|
636
|
+
* @returns {string} Expression with `@` prefixes removed.
|
|
637
|
+
*/
|
|
638
|
+
export function stripAmpVars(expr) {
|
|
639
|
+
return expr.replaceAll(/@(\w+)/g, '$1');
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Rewrite AMPscript code to be compatible with Marketing Cloud Next.
|
|
643
|
+
*
|
|
644
|
+
* Performs deterministic rewrites:
|
|
645
|
+
* - FormatDate(StringToDate(x), fmt) → FormatDate(x, fmt)
|
|
646
|
+
* - .NET → Java SimpleDateFormat format string conversions in FormatDate()
|
|
647
|
+
* - Lookup with odd arg count → annotated with comment
|
|
648
|
+
* - MCE-only functions → marked with %%-- NOT SUPPORTED IN MCN --%% annotation
|
|
649
|
+
* @param code - AMPscript source code to rewrite.
|
|
650
|
+
* @param options - Functions for MCN support checking and note retrieval.
|
|
651
|
+
* @returns {McnRewriteResult} Rewrite result with rewritten code, change log, and difficulty assessment.
|
|
652
|
+
*/
|
|
653
|
+
export function rewriteAmpForMcn(code, options) {
|
|
654
|
+
const { isMcnSupportedFn, getMcnNotesFn } = options;
|
|
655
|
+
const changes = [];
|
|
656
|
+
const nonMigratableItems = [];
|
|
657
|
+
let rewrittenCode = code;
|
|
658
|
+
// 1. Remove StringToDate() wrapper inside FormatDate() first argument
|
|
659
|
+
// FormatDate(StringToDate(x), fmt) → FormatDate(x, fmt)
|
|
660
|
+
const strToDatePattern = /FormatDate\s*\(\s*StringToDate\s*\(([^)]+)\)\s*,/gi;
|
|
661
|
+
if (strToDatePattern.test(rewrittenCode)) {
|
|
662
|
+
rewrittenCode = rewrittenCode.replaceAll(/FormatDate\s*\(\s*StringToDate\s*\(([^)]+)\)\s*,/gi, 'FormatDate($1,');
|
|
663
|
+
// Find affected lines for change tracking
|
|
664
|
+
const lines = code.split('\n');
|
|
665
|
+
for (const [i, line] of lines.entries()) {
|
|
666
|
+
if (/FormatDate\s*\(\s*StringToDate\s*\(/i.test(line)) {
|
|
667
|
+
changes.push({
|
|
668
|
+
line: i + 1,
|
|
669
|
+
type: 'rewritten',
|
|
670
|
+
description: 'Removed StringToDate() wrapper: FormatDate(StringToDate(x), fmt) → FormatDate(x, fmt)',
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// 2. Convert .NET format strings to Java SimpleDateFormat in FormatDate() calls
|
|
676
|
+
rewrittenCode = rewrittenCode.replaceAll(/FormatDate\s*\(\s*([^,]+),\s*"([^"]+)"\s*\)/gi, (match, arg1, formatStr) => {
|
|
677
|
+
let newFormat = formatStr;
|
|
678
|
+
let changed = false;
|
|
679
|
+
let hasShorthand = false;
|
|
680
|
+
// Check for standard shorthands
|
|
681
|
+
for (const shorthand of DOTNET_STANDARD_SHORTHANDS) {
|
|
682
|
+
if (new RegExp(`^${shorthand}$`).test(formatStr.trim())) {
|
|
683
|
+
hasShorthand = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (hasShorthand) {
|
|
687
|
+
// Annotate with comment about needing explicit format
|
|
688
|
+
return `FormatDate(${arg1}, "/* MANUAL_REWRITE_REQUIRED: Convert .NET standard shorthand '${formatStr}' to explicit Java SimpleDateFormat pattern */"${formatStr}")`;
|
|
689
|
+
}
|
|
690
|
+
// Apply .NET → Java replacements
|
|
691
|
+
for (const [pattern, replacement] of DOTNET_TO_JAVA_FORMAT_REPLACEMENTS) {
|
|
692
|
+
const before = newFormat;
|
|
693
|
+
newFormat = newFormat.replace(pattern, replacement);
|
|
694
|
+
if (newFormat !== before) {
|
|
695
|
+
changed = true;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (changed) {
|
|
699
|
+
return `FormatDate(${arg1}, "${newFormat}")`;
|
|
700
|
+
}
|
|
701
|
+
return match;
|
|
702
|
+
});
|
|
703
|
+
// Track format string changes
|
|
704
|
+
const codeLines = code.split('\n');
|
|
705
|
+
const rewrittenLines = rewrittenCode.split('\n');
|
|
706
|
+
for (const [i, codeLine] of codeLines.entries()) {
|
|
707
|
+
if (codeLine !== rewrittenLines[i] && /FormatDate/i.test(codeLine)) {
|
|
708
|
+
changes.push({
|
|
709
|
+
line: i + 1,
|
|
710
|
+
type: 'rewritten',
|
|
711
|
+
description: `Converted .NET format string to Java SimpleDateFormat in FormatDate()`,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// 3. Annotate Lookup() calls with odd argument counts
|
|
716
|
+
rewrittenCode = rewrittenCode.replaceAll(/\bLookup\s*\(([^)]+)\)/gi, (match, argsStr) => {
|
|
717
|
+
const argCount = countArgs(argsStr);
|
|
718
|
+
// Lookup takes: DE, returnCol, [searchCol, searchVal, ...]
|
|
719
|
+
// Min 2 args, then pairs after that → should be even number > 2 or exactly 2
|
|
720
|
+
// Odd count after first 2 = problem
|
|
721
|
+
if (argCount >= 3 && (argCount - 2) % 2 !== 0) {
|
|
722
|
+
return `${match} %%-- MCN NOTE: Lookup() requires search arguments in column/value pairs (even count after DE and return column). Current arg count (${argCount}) may cause an error in MCN. --%% `;
|
|
723
|
+
}
|
|
724
|
+
return match;
|
|
725
|
+
});
|
|
726
|
+
// 4. Mark MCE-only function calls with annotation
|
|
727
|
+
// Find all function calls and mark unsupported ones
|
|
728
|
+
const funcCallPattern = /\b([A-Z][A-Za-z]+)\s*\(/g;
|
|
729
|
+
let funcMatch;
|
|
730
|
+
const seenUnsupported = new Set();
|
|
731
|
+
while ((funcMatch = funcCallPattern.exec(code)) !== null) {
|
|
732
|
+
const fnName = funcMatch[1];
|
|
733
|
+
if (!isMcnSupportedFn(fnName) && fnName.length > 1) {
|
|
734
|
+
seenUnsupported.add(fnName);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const fnName of seenUnsupported) {
|
|
738
|
+
const mcnNotes = getMcnNotesFn(fnName);
|
|
739
|
+
const annotationPattern = new RegExp(String.raw `\b${fnName}\s*\(`, 'gi');
|
|
740
|
+
rewrittenCode = rewrittenCode.replace(annotationPattern, (m) => {
|
|
741
|
+
return `%%-- NOT SUPPORTED IN MCN: ${fnName}${mcnNotes ? ` — ${mcnNotes}` : ''} --%%\n${m}`;
|
|
742
|
+
});
|
|
743
|
+
// Find lines for tracking
|
|
744
|
+
for (const [i, codeLine] of codeLines.entries()) {
|
|
745
|
+
if (new RegExp(String.raw `\b${fnName}\s*\(`, 'i').test(codeLine)) {
|
|
746
|
+
changes.push({
|
|
747
|
+
line: i + 1,
|
|
748
|
+
type: 'annotated',
|
|
749
|
+
description: `${fnName}() is not supported in Marketing Cloud Next`,
|
|
750
|
+
});
|
|
751
|
+
nonMigratableItems.push({
|
|
752
|
+
line: i + 1,
|
|
753
|
+
code: codeLine.trim(),
|
|
754
|
+
reason: `${fnName}() is not available in Marketing Cloud Next`,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// 5. Check for CloudPages functions
|
|
760
|
+
const cloudFnPattern = /\b(CloudPagesURL|RequestParameter|QueryParameter|Redirect|MicrositeURL)\s*\(/gi;
|
|
761
|
+
let cloudMatch;
|
|
762
|
+
while ((cloudMatch = cloudFnPattern.exec(code)) !== null) {
|
|
763
|
+
const fnName = cloudMatch[1];
|
|
764
|
+
const lineNum = code.slice(0, cloudMatch.index).split('\n').length;
|
|
765
|
+
nonMigratableItems.push({
|
|
766
|
+
line: lineNum,
|
|
767
|
+
code: codeLines[lineNum - 1]?.trim() ?? fnName,
|
|
768
|
+
reason: `${fnName}() is a CloudPages-specific function and cannot run in Marketing Cloud Next`,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// Assess difficulty
|
|
772
|
+
const hasNotMigratable = nonMigratableItems.some((item) => item.reason.includes('CloudPages') || item.reason.includes('NOT SUPPORTED'));
|
|
773
|
+
const hasMcnNotes = Array.from(seenUnsupported).some((fn) => getMcnNotesFn(fn) !== null) ||
|
|
774
|
+
/FormatDate|StringToDate|Lookup/i.test(code);
|
|
775
|
+
const hasSsjs = /<script[^>]+runat/i.test(code);
|
|
776
|
+
let difficulty;
|
|
777
|
+
if (hasNotMigratable) {
|
|
778
|
+
difficulty = 'not-migratable';
|
|
779
|
+
}
|
|
780
|
+
else if (seenUnsupported.size > 0 || hasSsjs) {
|
|
781
|
+
difficulty = 'significant';
|
|
782
|
+
}
|
|
783
|
+
else if (hasMcnNotes) {
|
|
784
|
+
difficulty = 'minor';
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
difficulty = 'ready';
|
|
788
|
+
}
|
|
789
|
+
const summaryParts = [];
|
|
790
|
+
if (seenUnsupported.size > 0) {
|
|
791
|
+
summaryParts.push(`${seenUnsupported.size} MCE-only function(s) flagged`);
|
|
792
|
+
}
|
|
793
|
+
if (hasMcnNotes) {
|
|
794
|
+
summaryParts.push('behavioral differences noted (FormatDate, Lookup, or StringToDate)');
|
|
795
|
+
}
|
|
796
|
+
if (hasSsjs) {
|
|
797
|
+
summaryParts.push('SSJS blocks detected');
|
|
798
|
+
}
|
|
799
|
+
const summary = summaryParts.length > 0 ? summaryParts.join('; ') : 'No MCN issues found';
|
|
800
|
+
return { rewrittenCode, changes, nonMigratableItems, difficulty, summary };
|
|
801
|
+
}
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
// Utility helpers
|
|
804
|
+
// ---------------------------------------------------------------------------
|
|
805
|
+
/**
|
|
806
|
+
* Count the number of top-level comma-separated arguments in a function
|
|
807
|
+
* argument string (respects nested parentheses).
|
|
808
|
+
* @param argsStr - The argument string (contents between outer parens).
|
|
809
|
+
* @returns {number} Number of top-level arguments.
|
|
810
|
+
*/
|
|
811
|
+
export function countArgs(argsStr) {
|
|
812
|
+
if (!argsStr.trim())
|
|
813
|
+
return 0;
|
|
814
|
+
let depth = 0;
|
|
815
|
+
let count = 1;
|
|
816
|
+
for (const ch of argsStr) {
|
|
817
|
+
if (ch === '(' || ch === '[') {
|
|
818
|
+
depth++;
|
|
819
|
+
}
|
|
820
|
+
else if (ch === ')' || ch === ']') {
|
|
821
|
+
depth--;
|
|
822
|
+
}
|
|
823
|
+
else if (ch === ',' && depth === 0) {
|
|
824
|
+
count++;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return count;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Determine whether a SSJS code block contains only patterns that can be
|
|
831
|
+
* automatically converted to AMPscript. Returns true when no non-migratable
|
|
832
|
+
* patterns are found.
|
|
833
|
+
* @param blockCode - SSJS block code (without `<script>` tags).
|
|
834
|
+
* @returns {boolean} True when the block is likely convertible.
|
|
835
|
+
*/
|
|
836
|
+
export function isSsjsBlockConvertible(blockCode) {
|
|
837
|
+
for (const { pattern } of NON_MIGRATABLE_SSJS_PATTERNS) {
|
|
838
|
+
pattern.lastIndex = 0;
|
|
839
|
+
if (pattern.test(blockCode)) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
//# sourceMappingURL=conversion-rules.js.map
|