eslint-plugin-sfmc 3.0.0 → 4.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 +60 -57
- package/docs/rules/amp/no-mcn-unsupported.md +76 -0
- package/docs/rules/amp/no-unknown-function.md +2 -39
- package/docs/rules/hbs/no-mcn-unsupported.md +69 -0
- package/docs/rules/ssjs/no-mcn-unsupported.md +68 -0
- package/docs/rules/ssjs/no-unknown-function.md +4 -31
- package/package.json +1 -1
- package/src/index.js +22 -16
- package/src/rules/amp/no-mcn-unsupported.js +102 -0
- package/src/rules/amp/no-nested-ampscript-delimiter.js +58 -66
- package/src/rules/amp/no-smart-quotes.js +25 -31
- package/src/rules/amp/no-unknown-function.js +2 -41
- package/src/rules/hbs/no-mcn-unsupported.js +164 -0
- package/src/rules/ssjs/no-mcn-unsupported.js +182 -0
- package/src/rules/ssjs/no-unknown-function.js +1 -49
- package/src/rules/ssjs/ssjs-core-method-arity.js +22 -0
- package/docs/rules/hbs/helper-too-new-for-target.md +0 -62
- package/src/rules/hbs/helper-too-new-for-target.js +0 -94
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getHelper, bindingLookup } from 'handlebars-data';
|
|
2
|
+
|
|
3
|
+
import { simpleHelperName, isInvocation, BINDING_PATTERN } from './_shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flags Handlebars helpers and `{!$...}` data bindings that are not available in
|
|
7
|
+
* the targeted Marketing Cloud Next API version.
|
|
8
|
+
*
|
|
9
|
+
* Each helper/binding carries an `mcnSince` value = the MCN API version it first
|
|
10
|
+
* became available in, or `null`/unset when it was never supported in MCN.
|
|
11
|
+
*
|
|
12
|
+
* `apiVersion` semantics (shared with the AMPscript and SSJS MCN rules):
|
|
13
|
+
* - Not set / null: flag every helper/binding whose `mcnSince` is null/unset.
|
|
14
|
+
* Any item supported at some point in MCN (numeric `mcnSince`) passes.
|
|
15
|
+
* - Set to N: flag everything the null case flags, PLUS everything whose
|
|
16
|
+
* `mcnSince` is greater than N (too new for the target). Pass iff
|
|
17
|
+
* `mcnSince != null && mcnSince <= N`.
|
|
18
|
+
*
|
|
19
|
+
* The MCN Handlebars catalog currently has no null-`mcnSince` items, so with no
|
|
20
|
+
* `apiVersion` set nothing is flagged; e.g. `apiVersion: 65` flags the helpers
|
|
21
|
+
* introduced in 67, and `apiVersion: 40` flags every helper and binding.
|
|
22
|
+
*/
|
|
23
|
+
export default {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
'Disallow Handlebars helpers and bindings not available in the targeted Marketing Cloud Next API version',
|
|
29
|
+
recommended: true,
|
|
30
|
+
},
|
|
31
|
+
messages: {
|
|
32
|
+
helperNotSupported: "'{{name}}' is not supported in Marketing Cloud Next Handlebars.",
|
|
33
|
+
helperTooNew:
|
|
34
|
+
"'{{name}}' was introduced in Marketing Cloud Next API version {{since}}, which is newer than the targeted version {{target}}.",
|
|
35
|
+
bindingNotSupported: "'{{token}}' is not supported in Marketing Cloud Next Handlebars.",
|
|
36
|
+
bindingTooNew:
|
|
37
|
+
"'{{token}}' was introduced in Marketing Cloud Next API version {{since}}, which is newer than the targeted version {{target}}.",
|
|
38
|
+
},
|
|
39
|
+
schema: [
|
|
40
|
+
{
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
apiVersion: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description:
|
|
46
|
+
'The targeted Marketing Cloud Next API version (e.g. 65 = Winter \u{2019}26, 67 = Summer \u{2019}26). Helpers and bindings newer than this are flagged.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
create(context) {
|
|
55
|
+
const options = context.options[0] ?? {};
|
|
56
|
+
const apiVersion = typeof options.apiVersion === 'number' ? options.apiVersion : null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true when an item's `mcnSince` fails the target: it is null
|
|
60
|
+
* (never supported) or newer than the targeted API version.
|
|
61
|
+
*
|
|
62
|
+
* @param {number | null | undefined} since - The item's `mcnSince`.
|
|
63
|
+
* @returns {boolean} True when the item must be flagged.
|
|
64
|
+
*/
|
|
65
|
+
function isUnsupported(since) {
|
|
66
|
+
if (since === null || since === undefined) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return apiVersion !== null && since > apiVersion;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reports a helper when it is unavailable for the target.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} node - The AST node.
|
|
76
|
+
* @param {string | null} helperName - The simple helper name, when any.
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
function checkHelper(node, helperName) {
|
|
80
|
+
if (!helperName) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const helper = getHelper(helperName);
|
|
84
|
+
if (!helper) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const since = helper.mcnSince ?? null;
|
|
88
|
+
if (!isUnsupported(since)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (since === null) {
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: 'helperNotSupported',
|
|
95
|
+
data: { name: helper.name },
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
context.report({
|
|
99
|
+
node,
|
|
100
|
+
messageId: 'helperTooNew',
|
|
101
|
+
data: {
|
|
102
|
+
name: helper.name,
|
|
103
|
+
since: String(since),
|
|
104
|
+
target: String(apiVersion),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks an inline mustache or subexpression helper invocation.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} node - The AST node.
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
function checkInline(node) {
|
|
117
|
+
if (isInvocation(node)) {
|
|
118
|
+
checkHelper(node, simpleHelperName(node.path));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
MustacheStatement: checkInline,
|
|
124
|
+
SubExpression: checkInline,
|
|
125
|
+
HbsBlockStatement(node) {
|
|
126
|
+
checkHelper(node, simpleHelperName(node.path));
|
|
127
|
+
},
|
|
128
|
+
'Program:exit'() {
|
|
129
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
130
|
+
const text = sourceCode.getText();
|
|
131
|
+
BINDING_PATTERN.lastIndex = 0;
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = BINDING_PATTERN.exec(text)) !== null) {
|
|
134
|
+
const bindingName = match[1];
|
|
135
|
+
const binding = bindingLookup.get(bindingName.toLowerCase());
|
|
136
|
+
// Unknown bindings are handled by hbs-no-unknown-binding.
|
|
137
|
+
if (!binding) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const since = binding.mcnSince ?? null;
|
|
141
|
+
if (!isUnsupported(since)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const token = match[0];
|
|
145
|
+
const start = match.index;
|
|
146
|
+
const end = start + token.length;
|
|
147
|
+
const loc = {
|
|
148
|
+
start: sourceCode.getLocFromIndex(start),
|
|
149
|
+
end: sourceCode.getLocFromIndex(end),
|
|
150
|
+
};
|
|
151
|
+
if (since === null) {
|
|
152
|
+
context.report({ loc, messageId: 'bindingNotSupported', data: { token } });
|
|
153
|
+
} else {
|
|
154
|
+
context.report({
|
|
155
|
+
loc,
|
|
156
|
+
messageId: 'bindingTooNew',
|
|
157
|
+
data: { token, since: String(since), target: String(apiVersion) },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-mcn-unsupported
|
|
3
|
+
*
|
|
4
|
+
* Server-Side JavaScript as a whole is not available in Marketing Cloud Next.
|
|
5
|
+
* When targeting MCN, every SSJS API usage must be rewritten in AMPscript or
|
|
6
|
+
* Handlebars. This rule flags all SSJS API surface usage:
|
|
7
|
+
*
|
|
8
|
+
* - Platform.<...>() — any Platform namespace call (incl. Platform.Load)
|
|
9
|
+
* - HTTP.<method>() — any HTTP call
|
|
10
|
+
* - <coreVar>.<method>() — Core Library instance calls
|
|
11
|
+
* - <wsproxyVar>.<method>() — WSProxy instance calls
|
|
12
|
+
* - new Script.Util.WSProxy() — WSProxy construction
|
|
13
|
+
*
|
|
14
|
+
* The `apiVersion` option is accepted for parity with the AMPscript and
|
|
15
|
+
* Handlebars MCN rules, but currently has no effect: no MCN API version
|
|
16
|
+
* supports SSJS, so it always flags all SSJS usage.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { coreObjectNames } from 'ssjs-data';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'problem',
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
'Disallow all Server-Side JavaScript API usage, which is unavailable in Marketing Cloud Next',
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
ssjsNotSupportedInMcn:
|
|
30
|
+
'SSJS is not supported in Marketing Cloud Next. Rewrite this code in AMPscript or Handlebars.',
|
|
31
|
+
},
|
|
32
|
+
schema: [
|
|
33
|
+
{
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
apiVersion: {
|
|
37
|
+
type: 'number',
|
|
38
|
+
description:
|
|
39
|
+
'Targeted Marketing Cloud Next API version. Accepted for parity with the other MCN rules; currently has no effect because no version supports SSJS.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
create(context) {
|
|
48
|
+
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
49
|
+
const coreVariables = new Map();
|
|
50
|
+
// Track variable names assigned via new Script.Util.WSProxy()
|
|
51
|
+
const wsproxyVariables = new Set();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
VariableDeclaration(node) {
|
|
55
|
+
for (const declaration of node.declarations) {
|
|
56
|
+
if (
|
|
57
|
+
!declaration.init ||
|
|
58
|
+
!declaration.id ||
|
|
59
|
+
declaration.id.type !== 'Identifier'
|
|
60
|
+
) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const coreType = getCoreInitType(declaration.init);
|
|
64
|
+
if (coreType) {
|
|
65
|
+
coreVariables.set(declaration.id.name, coreType);
|
|
66
|
+
}
|
|
67
|
+
if (isWSProxyConstructor(declaration.init)) {
|
|
68
|
+
wsproxyVariables.add(declaration.id.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
AssignmentExpression(node) {
|
|
74
|
+
if (node.left.type !== 'Identifier') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const coreType = getCoreInitType(node.right);
|
|
78
|
+
if (coreType) {
|
|
79
|
+
coreVariables.set(node.left.name, coreType);
|
|
80
|
+
}
|
|
81
|
+
if (isWSProxyConstructor(node.right)) {
|
|
82
|
+
wsproxyVariables.add(node.left.name);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
NewExpression(node) {
|
|
87
|
+
if (isWSProxyConstructor(node)) {
|
|
88
|
+
context.report({ node: node.callee, messageId: 'ssjsNotSupportedInMcn' });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
CallExpression(node) {
|
|
93
|
+
const callee = node.callee;
|
|
94
|
+
if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier') {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Traverse the object chain to find the root identifier
|
|
99
|
+
// (handles de.Rows.Retrieve() where root is `de`,
|
|
100
|
+
// and Platform.Function.X() where root is `Platform`).
|
|
101
|
+
let rootNode = callee.object;
|
|
102
|
+
while (rootNode.type === 'MemberExpression') {
|
|
103
|
+
rootNode = rootNode.object;
|
|
104
|
+
}
|
|
105
|
+
if (rootNode.type !== 'Identifier') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rootName = rootNode.name;
|
|
109
|
+
|
|
110
|
+
const isSfmcApiCall =
|
|
111
|
+
rootName === 'Platform' ||
|
|
112
|
+
rootName === 'HTTP' ||
|
|
113
|
+
coreVariables.has(rootName) ||
|
|
114
|
+
wsproxyVariables.has(rootName);
|
|
115
|
+
|
|
116
|
+
if (isSfmcApiCall) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: callee.property,
|
|
119
|
+
messageId: 'ssjsNotSupportedInMcn',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* If `node` is a Core Library Init call (e.g. `DataExtension.Init("key")`),
|
|
129
|
+
* return the Core Library type name; otherwise return null.
|
|
130
|
+
*
|
|
131
|
+
* @param {import('eslint').Rule.Node} node - AST node to inspect
|
|
132
|
+
* @returns {string | null} Core Library type name, or null when not a Core Library Init call
|
|
133
|
+
*/
|
|
134
|
+
function getCoreInitType(node) {
|
|
135
|
+
if (!node || node.type !== 'CallExpression') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const callee = node.callee;
|
|
139
|
+
if (callee.type !== 'MemberExpression') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
|
|
146
|
+
return callee.object.name;
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
callee.object.type === 'MemberExpression' &&
|
|
150
|
+
callee.object.object.type === 'Identifier' &&
|
|
151
|
+
callee.object.property.type === 'Identifier'
|
|
152
|
+
) {
|
|
153
|
+
const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
|
|
154
|
+
if (coreObjectNames.has(fullName)) {
|
|
155
|
+
return fullName;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns true if `node` is `new Script.Util.WSProxy()`.
|
|
163
|
+
*
|
|
164
|
+
* @param {import('eslint').Rule.Node} node - AST node to inspect
|
|
165
|
+
* @returns {boolean} true when the node is a WSProxy constructor call
|
|
166
|
+
*/
|
|
167
|
+
function isWSProxyConstructor(node) {
|
|
168
|
+
if (!node || node.type !== 'NewExpression') {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const c = node.callee;
|
|
172
|
+
return (
|
|
173
|
+
c.type === 'MemberExpression' &&
|
|
174
|
+
c.property.type === 'Identifier' &&
|
|
175
|
+
c.property.name === 'WSProxy' &&
|
|
176
|
+
c.object.type === 'MemberExpression' &&
|
|
177
|
+
c.object.property.type === 'Identifier' &&
|
|
178
|
+
c.object.property.name === 'Util' &&
|
|
179
|
+
c.object.object.type === 'Identifier' &&
|
|
180
|
+
c.object.object.name === 'Script'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -49,29 +49,11 @@ export default {
|
|
|
49
49
|
unknownCoreMethod:
|
|
50
50
|
"'{{method}}' is not a known method of {{objectType}}. Available methods: {{available}}.",
|
|
51
51
|
unknownWsproxyMethod: "'{{name}}' is not a recognized WSProxy method.",
|
|
52
|
-
ssjsNotSupportedInMcn:
|
|
53
|
-
'SSJS is not supported in Marketing Cloud Next. Use AMPscript instead.',
|
|
54
52
|
},
|
|
55
|
-
schema: [
|
|
56
|
-
{
|
|
57
|
-
type: 'object',
|
|
58
|
-
properties: {
|
|
59
|
-
target: {
|
|
60
|
-
type: 'string',
|
|
61
|
-
enum: ['engagement', 'next'],
|
|
62
|
-
description:
|
|
63
|
-
"Target platform. Set to 'next' to flag all SSJS API calls as unsupported in Marketing Cloud Next.",
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
53
|
+
schema: [],
|
|
69
54
|
},
|
|
70
55
|
|
|
71
56
|
create(context) {
|
|
72
|
-
const options = context.options[0] ?? {};
|
|
73
|
-
const isTargetNext = options.target === 'next';
|
|
74
|
-
|
|
75
57
|
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
76
58
|
const coreVariables = new Map();
|
|
77
59
|
// Track variable names assigned via new Script.Util.WSProxy()
|
|
@@ -122,36 +104,6 @@ export default {
|
|
|
122
104
|
|
|
123
105
|
const methodName = property.name;
|
|
124
106
|
|
|
125
|
-
// ── MCN target: flag all SSJS API calls as unsupported ────────
|
|
126
|
-
if (isTargetNext) {
|
|
127
|
-
// Only report on Platform.<NS>.<method>(), HTTP.<method>(),
|
|
128
|
-
// Core Library instance calls, or WSProxy calls — not bare JS.
|
|
129
|
-
const isPlatformCall =
|
|
130
|
-
callee.object.type === 'MemberExpression' &&
|
|
131
|
-
callee.object.object.type === 'Identifier' &&
|
|
132
|
-
callee.object.object.name === 'Platform';
|
|
133
|
-
const isHttpCall =
|
|
134
|
-
callee.object.type === 'Identifier' && callee.object.name === 'HTTP';
|
|
135
|
-
|
|
136
|
-
// Traverse the object chain to find the root identifier
|
|
137
|
-
// (handles de.Rows.Retrieve() where root is `de`)
|
|
138
|
-
let rootNode = callee.object;
|
|
139
|
-
while (rootNode.type === 'MemberExpression') {
|
|
140
|
-
rootNode = rootNode.object;
|
|
141
|
-
}
|
|
142
|
-
const isInstanceCall =
|
|
143
|
-
rootNode.type === 'Identifier' &&
|
|
144
|
-
(coreVariables.has(rootNode.name) || wsproxyVariables.has(rootNode.name));
|
|
145
|
-
|
|
146
|
-
if (isPlatformCall || isHttpCall || isInstanceCall) {
|
|
147
|
-
context.report({
|
|
148
|
-
node: property,
|
|
149
|
-
messageId: 'ssjsNotSupportedInMcn',
|
|
150
|
-
});
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
107
|
// ── Platform.<NS>.<method>() ──────────────────────────────────
|
|
156
108
|
if (
|
|
157
109
|
callee.object.type === 'MemberExpression' &&
|
|
@@ -135,6 +135,28 @@ export default {
|
|
|
135
135
|
callee.property,
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Instance sub-path: de.Rows.Add(...) where `de` is a tracked
|
|
142
|
+
// DataExtension.Init(...) instance. Substitute the instance's
|
|
143
|
+
// core type for the leftmost identifier and resolve the class key.
|
|
144
|
+
if (objectPath) {
|
|
145
|
+
const segments = objectPath.split('.');
|
|
146
|
+
const rootCoreType = coreVariables.get(segments[0]);
|
|
147
|
+
if (rootCoreType) {
|
|
148
|
+
const resolvedPath = [rootCoreType, ...segments.slice(1)].join('.');
|
|
149
|
+
const classLookup = coreMethodArityLookup.get(resolvedPath.toLowerCase());
|
|
150
|
+
if (classLookup) {
|
|
151
|
+
const entry = classLookup.get(methodName.toLowerCase());
|
|
152
|
+
checkArity(
|
|
153
|
+
entry,
|
|
154
|
+
node.arguments,
|
|
155
|
+
`${resolvedPath}.${methodName}`,
|
|
156
|
+
callee.property,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
138
160
|
}
|
|
139
161
|
},
|
|
140
162
|
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# `sfmc/hbs-helper-too-new-for-target`
|
|
2
|
-
|
|
3
|
-
> Disallow Handlebars helpers newer than the targeted Marketing Cloud Next API version.
|
|
4
|
-
|
|
5
|
-
| | |
|
|
6
|
-
|---|---|
|
|
7
|
-
| **Type** | `problem` |
|
|
8
|
-
| **Default severity** | `off` (opt-in — requires the `apiVersion` option) |
|
|
9
|
-
| **Fixable** | — |
|
|
10
|
-
|
|
11
|
-
## Why This Rule Exists
|
|
12
|
-
|
|
13
|
-
Marketing Cloud Next ships new Handlebars helpers over time. Each helper in the catalog records the API version that introduced it (`mcnSince`). If your account targets an older API version, a newer helper is not yet available and fails at render time.
|
|
14
|
-
|
|
15
|
-
This rule flags any known helper whose introducing version (`mcnSince`) is greater than the API version you target. For example, `dateAdd` first shipped in MCN API version 67 (Summer '26); using it while targeting API version 65 (Winter '26) is reported.
|
|
16
|
-
|
|
17
|
-
The rule does nothing until you tell it which API version you target via the `apiVersion` option, which is why it defaults to `off` even in the `-next` configs. Set the option to opt in.
|
|
18
|
-
|
|
19
|
-
## Settings
|
|
20
|
-
|
|
21
|
-
| Setting | Values | Default |
|
|
22
|
-
|---------|--------|---------|
|
|
23
|
-
| severity | `"error"` \| `"warn"` \| `"off"` | `"off"` |
|
|
24
|
-
|
|
25
|
-
### Options
|
|
26
|
-
|
|
27
|
-
| Option | Type | Default | Description |
|
|
28
|
-
|--------|------|---------|-------------|
|
|
29
|
-
| `apiVersion` | `number` | — | The targeted Marketing Cloud Next API version (e.g. `65` = Winter '26, `67` = Summer '26). Helpers introduced after this version are flagged. When omitted, no helper is flagged. |
|
|
30
|
-
|
|
31
|
-
## Examples
|
|
32
|
-
|
|
33
|
-
Configure the rule with the API version you target:
|
|
34
|
-
|
|
35
|
-
```js
|
|
36
|
-
// eslint.config.js
|
|
37
|
-
rules: {
|
|
38
|
-
'sfmc/hbs-helper-too-new-for-target': ['error', { apiVersion: 65 }]
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Not allowed (with `apiVersion: 65`):**
|
|
43
|
-
|
|
44
|
-
```handlebars
|
|
45
|
-
{{! dateAdd was introduced in API version 67 }}
|
|
46
|
-
{{dateAdd order.createdAt 7 "days"}}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Allowed (with `apiVersion: 65`):**
|
|
50
|
-
|
|
51
|
-
```handlebars
|
|
52
|
-
{{formatDate order.createdAt "yyyy-MM-dd"}}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## When to Disable
|
|
56
|
-
|
|
57
|
-
Leave this rule `off` (its default) if you always target the latest MCN API version, or if you do not need to guard against version drift.
|
|
58
|
-
|
|
59
|
-
```js
|
|
60
|
-
// eslint.config.js
|
|
61
|
-
rules: { 'sfmc/hbs-helper-too-new-for-target': 'off' }
|
|
62
|
-
```
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { getHelper } from 'handlebars-data';
|
|
2
|
-
|
|
3
|
-
import { simpleHelperName, isInvocation } from './_shared.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Flags known Handlebars helpers whose introducing Marketing Cloud Next API
|
|
7
|
-
* version (`mcnSince`) is newer than the API version the template targets.
|
|
8
|
-
*
|
|
9
|
-
* For example, `dateAdd` first shipped in MCN API version 67 (Summer '26); using
|
|
10
|
-
* it while targeting API version 65 (Winter '26) is reported.
|
|
11
|
-
*
|
|
12
|
-
* The target API version is configured via the `apiVersion` option. When it is
|
|
13
|
-
* omitted, no helper is flagged (all helpers are considered available).
|
|
14
|
-
*/
|
|
15
|
-
export default {
|
|
16
|
-
meta: {
|
|
17
|
-
type: 'problem',
|
|
18
|
-
docs: {
|
|
19
|
-
description:
|
|
20
|
-
'Disallow Handlebars helpers newer than the targeted Marketing Cloud Next API version',
|
|
21
|
-
recommended: true,
|
|
22
|
-
},
|
|
23
|
-
messages: {
|
|
24
|
-
helperTooNew:
|
|
25
|
-
"'{{name}}' was introduced in Marketing Cloud Next API version {{since}}, which is newer than the targeted version {{target}}.",
|
|
26
|
-
},
|
|
27
|
-
schema: [
|
|
28
|
-
{
|
|
29
|
-
type: 'object',
|
|
30
|
-
properties: {
|
|
31
|
-
apiVersion: {
|
|
32
|
-
type: 'number',
|
|
33
|
-
description:
|
|
34
|
-
'The targeted Marketing Cloud Next API version (e.g. 65 = Winter \u{2019}26, 67 = Summer \u{2019}26). Helpers newer than this are flagged.',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
additionalProperties: false,
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
create(context) {
|
|
43
|
-
const options = context.options[0] ?? {};
|
|
44
|
-
const apiVersion = typeof options.apiVersion === 'number' ? options.apiVersion : null;
|
|
45
|
-
if (apiVersion === null) {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Reports a helper when its introducing version exceeds the target.
|
|
51
|
-
*
|
|
52
|
-
* @param {object} node - The AST node.
|
|
53
|
-
* @param {string | null} helperName - The simple helper name, when any.
|
|
54
|
-
* @returns {void}
|
|
55
|
-
*/
|
|
56
|
-
function check(node, helperName) {
|
|
57
|
-
if (!helperName) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const helper = getHelper(helperName);
|
|
61
|
-
if (helper && helper.mcnSince > apiVersion) {
|
|
62
|
-
context.report({
|
|
63
|
-
node,
|
|
64
|
-
messageId: 'helperTooNew',
|
|
65
|
-
data: {
|
|
66
|
-
name: helper.name,
|
|
67
|
-
since: String(helper.mcnSince),
|
|
68
|
-
target: String(apiVersion),
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Checks an inline mustache or subexpression invocation.
|
|
76
|
-
*
|
|
77
|
-
* @param {object} node - The AST node.
|
|
78
|
-
* @returns {void}
|
|
79
|
-
*/
|
|
80
|
-
function checkInline(node) {
|
|
81
|
-
if (isInvocation(node)) {
|
|
82
|
-
check(node, simpleHelperName(node.path));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
MustacheStatement: checkInline,
|
|
88
|
-
SubExpression: checkInline,
|
|
89
|
-
HbsBlockStatement(node) {
|
|
90
|
-
check(node, simpleHelperName(node.path));
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
};
|