eslint-plugin-sfmc 2.6.0 → 3.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 +72 -63
- package/docs/rules/hbs/helper-arity.md +55 -0
- package/docs/rules/hbs/helper-too-new-for-target.md +62 -0
- package/docs/rules/hbs/no-unknown-binding.md +49 -0
- package/docs/rules/hbs/no-unknown-helper.md +60 -0
- package/docs/rules/hbs/no-unsupported-construct.md +65 -0
- package/package.json +14 -12
- package/src/ampscript-parser.js +1 -1
- package/src/ampscript-processor.js +22 -16
- package/src/handlebars-parser.js +195 -0
- package/src/index.js +90 -11
- package/src/processor.js +76 -16
- package/src/rules/amp/{arg-types.js → argument-types.js} +16 -12
- package/src/rules/amp/function-arity.js +9 -9
- package/src/rules/amp/no-inline-statement.js +5 -5
- package/src/rules/amp/no-loop-counter-assign.js +7 -7
- package/src/rules/amp/no-nested-ampscript-delimiter.js +1 -1
- package/src/rules/amp/no-smart-quotes.js +18 -18
- package/src/rules/amp/no-unknown-function.js +16 -3
- package/src/rules/amp/prefer-attribute-value.js +7 -10
- package/src/rules/amp/require-rowcount-check.js +12 -15
- package/src/rules/amp/require-variable-declaration.js +3 -3
- package/src/rules/hbs/_shared.js +116 -0
- package/src/rules/hbs/helper-arity.js +97 -0
- package/src/rules/hbs/helper-too-new-for-target.js +94 -0
- package/src/rules/hbs/no-unknown-binding.js +74 -0
- package/src/rules/hbs/no-unknown-helper.js +79 -0
- package/src/rules/hbs/no-unsupported-construct.js +70 -0
- package/src/rules/ssjs/cache-loop-length.js +14 -17
- package/src/rules/ssjs/no-deprecated-function.js +8 -8
- package/src/rules/ssjs/no-hardcoded-credentials.js +3 -6
- package/src/rules/ssjs/no-property-call.js +6 -9
- package/src/rules/ssjs/no-treatascontent-injection.js +6 -13
- package/src/rules/ssjs/no-unavailable-method.js +22 -22
- package/src/rules/ssjs/no-unknown-function.js +16 -12
- package/src/rules/ssjs/no-unsupported-syntax.js +5 -5
- package/src/rules/ssjs/{prefer-parsejson-safe-arg.js → prefer-parsejson-safe-argument.js} +13 -21
- package/src/rules/ssjs/require-hasownproperty.js +54 -42
- package/src/rules/ssjs/require-platform-load-order.js +1 -1
- package/src/rules/ssjs/require-platform-load.js +6 -6
- package/src/rules/ssjs/{ssjs-arg-types.js → ssjs-argument-types.js} +59 -46
- package/src/rules/ssjs/ssjs-core-method-arity.js +19 -15
- /package/src/rules/amp/{no-var-redeclaration.js → no-variable-redeclaration.js} +0 -0
package/README.md
CHANGED
|
@@ -39,18 +39,18 @@ Add the following to your `.vscode/settings.json`:
|
|
|
39
39
|
|
|
40
40
|
```json
|
|
41
41
|
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
"eslint.validate": [
|
|
43
|
+
"javascript",
|
|
44
|
+
"javascriptreact",
|
|
45
|
+
"typescript",
|
|
46
|
+
"typescriptreact",
|
|
47
|
+
"html",
|
|
48
|
+
"vue",
|
|
49
|
+
"markdown",
|
|
50
|
+
"ampscript",
|
|
51
|
+
"ssjs",
|
|
52
|
+
"sfmc"
|
|
53
|
+
]
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -74,13 +74,13 @@ Add the following to your `.vscode/settings.json`:
|
|
|
74
74
|
|
|
75
75
|
Use the `-next` config variants when targeting **Marketing Cloud Next (MCN)**. MCN supports only a subset of AMPscript functions and does **not** support SSJS at all.
|
|
76
76
|
|
|
77
|
-
| Config
|
|
78
|
-
|
|
|
79
|
-
| `sfmc.configs['ampscript-next']`
|
|
80
|
-
| `sfmc.configs['ssjs-next']`
|
|
81
|
-
| `sfmc.configs['recommended-next']` | Both of the above
|
|
82
|
-
| `sfmc.configs['embedded-next']`
|
|
83
|
-
| `sfmc.configs['strict-next']`
|
|
77
|
+
| Config | Files | What it does |
|
|
78
|
+
| ---------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------- |
|
|
79
|
+
| `sfmc.configs['ampscript-next']` | `**/*.ampscript`, `**/*.amp` | AMPscript rules + flags functions unsupported in MCN (single config object) |
|
|
80
|
+
| `sfmc.configs['ssjs-next']` | `**/*.ssjs` | Flags all SSJS API calls as MCN-unsupported; all other SSJS quality rules disabled |
|
|
81
|
+
| `sfmc.configs['recommended-next']` | Both of the above | AMPscript MCN-aware + SSJS flagged for standalone files |
|
|
82
|
+
| `sfmc.configs['embedded-next']` | `**/*.html` | AMPscript MCN-aware + SSJS flagged for HTML-embedded code |
|
|
83
|
+
| `sfmc.configs['strict-next']` | All of the above + HTML | All AMPscript rules at `error` severity + MCN flag; SSJS fully flagged |
|
|
84
84
|
|
|
85
85
|
`recommended-next`, `embedded-next`, and `strict-next` are arrays — spread them with `...`.
|
|
86
86
|
|
|
@@ -88,58 +88,67 @@ Use the `-next` config variants when targeting **Marketing Cloud Next (MCN)**. M
|
|
|
88
88
|
// eslint.config.js — targeting Marketing Cloud Next
|
|
89
89
|
import sfmc from 'eslint-plugin-sfmc';
|
|
90
90
|
|
|
91
|
-
export default [
|
|
92
|
-
...sfmc.configs['recommended-next'],
|
|
93
|
-
...sfmc.configs['embedded-next'],
|
|
94
|
-
];
|
|
91
|
+
export default [...sfmc.configs['recommended-next'], ...sfmc.configs['embedded-next']];
|
|
95
92
|
```
|
|
96
93
|
|
|
97
94
|
## AMPscript Rules (`amp-*`)
|
|
98
95
|
|
|
99
|
-
| Rule | Default | Description
|
|
100
|
-
| ------------------------------------------------------------------------------------------- | ------- |
|
|
101
|
-
| [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md) | `error` | Disallow calls to unknown AMPscript functions
|
|
102
|
-
| [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md) | `error` | Enforce correct argument counts
|
|
96
|
+
| Rule | Default | Description |
|
|
97
|
+
| ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------ |
|
|
98
|
+
| [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md) | `error` | Disallow calls to unknown AMPscript functions |
|
|
99
|
+
| [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md) | `error` | Enforce correct argument counts |
|
|
103
100
|
| [`sfmc/amp-arg-types`](docs/rules/amp/arg-types.md) | `error` | Check that literal arguments match expected parameter types and allowed values |
|
|
104
|
-
| [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md) | `error` | Require `set` to have a target variable
|
|
105
|
-
| [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md) | `error` | Disallow smart/curly quotes in strings
|
|
106
|
-
| [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md) | `warn` | Disallow re-declaring a variable with `var`
|
|
107
|
-
| [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md) | `warn` | Disallow empty `%%[ ]%%` blocks
|
|
108
|
-
| [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md) | `warn` | Disallow assigning to the `for` loop counter
|
|
109
|
-
| [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md) | `warn` | Disallow statements inside inline expressions
|
|
110
|
-
| [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md) | `warn` | Flag deprecated functions and suggest replacements
|
|
111
|
-
| [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md) | `warn` | Enforce variable naming convention
|
|
112
|
-
| [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md) | `warn` | Disallow IF with empty THEN branch
|
|
113
|
-
| [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md) | `warn` | Require RowCount check before FOR on LookupRows
|
|
114
|
-
| [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md) | `warn` | Disallow HTML comments inside AMPscript blocks
|
|
115
|
-
| [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md) | `warn` | Disallow JS-style `//` line comments in AMPscript
|
|
116
|
-
| [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md) | `error` | Disallow `<script>` tags nested inside AMPscript script tags
|
|
117
|
-
| [`sfmc/amp-no-nested-ampscript-delimiter`](docs/rules/amp/no-nested-ampscript-delimiter.md) | `error` | Disallow AMPscript delimiters nested inside AMPscript blocks
|
|
118
|
-
| [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md) | off | Prefer `AttributeValue()` over bare personalization
|
|
119
|
-
| [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md) | off | Require `var` before `set`
|
|
120
|
-
| [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md) | off | Flag functions unavailable in email context
|
|
101
|
+
| [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md) | `error` | Require `set` to have a target variable |
|
|
102
|
+
| [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md) | `error` | Disallow smart/curly quotes in strings |
|
|
103
|
+
| [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md) | `warn` | Disallow re-declaring a variable with `var` |
|
|
104
|
+
| [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md) | `warn` | Disallow empty `%%[ ]%%` blocks |
|
|
105
|
+
| [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md) | `warn` | Disallow assigning to the `for` loop counter |
|
|
106
|
+
| [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md) | `warn` | Disallow statements inside inline expressions |
|
|
107
|
+
| [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md) | `warn` | Flag deprecated functions and suggest replacements |
|
|
108
|
+
| [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md) | `warn` | Enforce variable naming convention |
|
|
109
|
+
| [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md) | `warn` | Disallow IF with empty THEN branch |
|
|
110
|
+
| [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md) | `warn` | Require RowCount check before FOR on LookupRows |
|
|
111
|
+
| [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md) | `warn` | Disallow HTML comments inside AMPscript blocks |
|
|
112
|
+
| [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md) | `warn` | Disallow JS-style `//` line comments in AMPscript |
|
|
113
|
+
| [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md) | `error` | Disallow `<script>` tags nested inside AMPscript script tags |
|
|
114
|
+
| [`sfmc/amp-no-nested-ampscript-delimiter`](docs/rules/amp/no-nested-ampscript-delimiter.md) | `error` | Disallow AMPscript delimiters nested inside AMPscript blocks |
|
|
115
|
+
| [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md) | off | Prefer `AttributeValue()` over bare personalization |
|
|
116
|
+
| [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md) | off | Require `var` before `set` |
|
|
117
|
+
| [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md) | off | Flag functions unavailable in email context |
|
|
121
118
|
|
|
122
119
|
## SSJS Rules (`ssjs-*`)
|
|
123
120
|
|
|
124
|
-
| Rule
|
|
125
|
-
|
|
|
126
|
-
| [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md)
|
|
127
|
-
| [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md)
|
|
128
|
-
| [`sfmc/ssjs-no-unknown-function`](docs/rules/ssjs/no-unknown-function.md)
|
|
129
|
-
| [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md)
|
|
130
|
-
| [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md)
|
|
131
|
-
| [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md)
|
|
132
|
-
| [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md)
|
|
133
|
-
| [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md)
|
|
134
|
-
| [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md)
|
|
135
|
-
| [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md)
|
|
136
|
-
| [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md)
|
|
137
|
-
| [`sfmc/ssjs-no-unavailable-method`](docs/rules/ssjs/no-unavailable-method.md)
|
|
138
|
-
| [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md)
|
|
139
|
-
| [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md)
|
|
140
|
-
| [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md)
|
|
141
|
-
| [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md)
|
|
142
|
-
| [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md)
|
|
121
|
+
| Rule | Default | Description |
|
|
122
|
+
| ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
|
|
123
|
+
| [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md) | `error` | Require `Platform.Load("core")` before Core or requiresCoreLoad globals |
|
|
124
|
+
| [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md) | `error` | Flag ES6+ syntax not supported by SFMC |
|
|
125
|
+
| [`sfmc/ssjs-no-unknown-function`](docs/rules/ssjs/no-unknown-function.md) | `error` | Disallow unknown methods on Platform.\*, HTTP, Core Library, and WSProxy |
|
|
126
|
+
| [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ContentAreaObj) |
|
|
127
|
+
| [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
|
|
128
|
+
| [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
|
|
129
|
+
| [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
|
|
130
|
+
| [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
|
|
131
|
+
| [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md) | `warn` | Require caching `.length` in for-loops |
|
|
132
|
+
| [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
|
|
133
|
+
| [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
|
|
134
|
+
| [`sfmc/ssjs-no-unavailable-method`](docs/rules/ssjs/no-unavailable-method.md) | `warn` | Flag Array/String methods unavailable or broken in SFMC's ES3 engine |
|
|
135
|
+
| [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
|
|
136
|
+
| [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
|
|
137
|
+
| [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
|
|
138
|
+
| [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md) | `warn` | Enforce correct argument counts for Core Library object methods |
|
|
139
|
+
| [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md) | `warn` | Check that literal arguments match expected parameter types |
|
|
140
|
+
|
|
141
|
+
## Handlebars Rules (`hbs-*`)
|
|
142
|
+
|
|
143
|
+
Handlebars is the templating language for **Marketing Cloud Next (MCN)** only. These rules are enabled at `error` severity in the `-next` configs and are `off` in the classic (Engagement) configs — in classic SFMC, `{{...}}` is plain content and must not be flagged.
|
|
144
|
+
|
|
145
|
+
| Rule | Default (`-next`) | Description |
|
|
146
|
+
| ----------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------- |
|
|
147
|
+
| [`sfmc/hbs-no-unknown-helper`](docs/rules/hbs/no-unknown-helper.md) | `error` | Disallow helper invocations that are not part of the MCN catalog |
|
|
148
|
+
| [`sfmc/hbs-no-unknown-binding`](docs/rules/hbs/no-unknown-binding.md) | `error` | Disallow unknown `{!$...}` built-in data bindings |
|
|
149
|
+
| [`sfmc/hbs-helper-arity`](docs/rules/hbs/helper-arity.md) | `error` | Enforce correct positional-argument counts for known helpers |
|
|
150
|
+
| [`sfmc/hbs-no-unsupported-construct`](docs/rules/hbs/no-unsupported-construct.md) | `error` | Disallow constructs unsupported by the MCN engine (partials, decorators, `log`) |
|
|
151
|
+
| [`sfmc/hbs-helper-too-new-for-target`](docs/rules/hbs/helper-too-new-for-target.md) | off | Flag helpers newer than the targeted MCN API version (opt-in via `apiVersion`) |
|
|
143
152
|
|
|
144
153
|
## Processors
|
|
145
154
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# `sfmc/hbs-helper-arity`
|
|
2
|
+
|
|
3
|
+
> Enforce correct positional-argument counts for known Marketing Cloud Next Handlebars helpers.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `error` in the `-next` configs; `off` in classic configs |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Each Marketing Cloud Next helper has a fixed set of positional parameters defined in the catalog. Calling a helper with too few required arguments, or with more arguments than it accepts, fails at render time. This rule validates every helper invocation against the helper's `params` definition.
|
|
14
|
+
|
|
15
|
+
Only **positional** arguments are checked. Helpers with a variadic trailing parameter (for example `concat`, `and`, `set`) have no upper bound. Hash (named) arguments are not counted as positional arguments. A bare `{{foo}}` mustache with no arguments is a data binding, not an invocation, and is skipped.
|
|
16
|
+
|
|
17
|
+
Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
|
|
18
|
+
|
|
19
|
+
## Settings
|
|
20
|
+
|
|
21
|
+
| Setting | Values | Default |
|
|
22
|
+
|---------|--------|---------|
|
|
23
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
|
|
24
|
+
|
|
25
|
+
This rule has no configuration options.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
**Not allowed:**
|
|
30
|
+
|
|
31
|
+
```handlebars
|
|
32
|
+
{{! formatDate requires a value and a format string }}
|
|
33
|
+
{{formatDate order.createdAt}}
|
|
34
|
+
|
|
35
|
+
{{! uppercase takes exactly one argument }}
|
|
36
|
+
{{uppercase firstName lastName}}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Allowed:**
|
|
40
|
+
|
|
41
|
+
```handlebars
|
|
42
|
+
{{formatDate order.createdAt "yyyy-MM-dd"}}
|
|
43
|
+
|
|
44
|
+
{{uppercase firstName}}
|
|
45
|
+
|
|
46
|
+
{{! concat is variadic — any number of arguments is fine }}
|
|
47
|
+
{{concat firstName " " lastName}}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## When to Disable
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// eslint.config.js
|
|
54
|
+
rules: { 'sfmc/hbs-helper-arity': 'off' }
|
|
55
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# `sfmc/hbs-no-unknown-binding`
|
|
2
|
+
|
|
3
|
+
> Disallow unknown `{!$...}` built-in data bindings in Marketing Cloud Next Handlebars.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `error` in the `-next` configs; `off` in classic configs |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Marketing Cloud Next exposes a fixed set of built-in data bindings written as `{!$namespace.Field}` tokens (for example `{!$Context.MessageContext}`). These `{!$...}` tokens are **not** Handlebars helper syntax — the Handlebars parser treats them as literal content — so this rule scans the raw source text with a regex pass rather than walking the AST, mirroring the `handlebars/unknown-binding` diagnostic emitted by the language server.
|
|
14
|
+
|
|
15
|
+
An unrecognized binding name is a typo or a reference to a namespace/field that MCN does not provide, and resolves to empty output at render time. The rule offers a "did you mean" suggestion when a close match exists.
|
|
16
|
+
|
|
17
|
+
Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
|
|
18
|
+
|
|
19
|
+
## Settings
|
|
20
|
+
|
|
21
|
+
| Setting | Values | Default |
|
|
22
|
+
|---------|--------|---------|
|
|
23
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
|
|
24
|
+
|
|
25
|
+
This rule has no configuration options.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
**Not allowed:**
|
|
30
|
+
|
|
31
|
+
```handlebars
|
|
32
|
+
{{! 'Contect' is not a known binding namespace }}
|
|
33
|
+
{!$Contect.MessageContext}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Allowed:**
|
|
37
|
+
|
|
38
|
+
```handlebars
|
|
39
|
+
{!$Context.MessageContext}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## When to Disable
|
|
43
|
+
|
|
44
|
+
Disable only if you use a binding that is available in your MCN account but missing from the bundled catalog (please report it so the catalog can be updated).
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// eslint.config.js
|
|
48
|
+
rules: { 'sfmc/hbs-no-unknown-binding': 'off' }
|
|
49
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# `sfmc/hbs-no-unknown-helper`
|
|
2
|
+
|
|
3
|
+
> Disallow Handlebars helper invocations that are not part of the Marketing Cloud Next catalog.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `error` in the `-next` configs; `off` in classic configs |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Marketing Cloud Next (MCN) runs a locked-down Handlebars engine that cannot register custom helpers. Every helper name must exist in the published MCN catalog. An unknown helper name is a typo or an unsupported construct that fails at render time. This rule flags any helper invocation whose name is not in the catalog and offers a "did you mean" suggestion when a close match exists.
|
|
14
|
+
|
|
15
|
+
A bare `{{foo}}` mustache with no arguments is treated as a **data binding**, not a helper invocation, and is not flagged by this rule. Helper detection applies to inline mustaches with arguments, subexpressions, and block helpers.
|
|
16
|
+
|
|
17
|
+
Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs. In classic (Marketing Cloud Engagement) configs it is disabled because `{{...}}` is plain content there.
|
|
18
|
+
|
|
19
|
+
## Settings
|
|
20
|
+
|
|
21
|
+
| Setting | Values | Default |
|
|
22
|
+
|---------|--------|---------|
|
|
23
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
|
|
24
|
+
|
|
25
|
+
This rule has no configuration options.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
**Not allowed:**
|
|
30
|
+
|
|
31
|
+
```handlebars
|
|
32
|
+
{{! 'formatDates' is not a catalog helper — did you mean 'formatDate'? }}
|
|
33
|
+
{{formatDates order.createdAt "yyyy-MM-dd"}}
|
|
34
|
+
|
|
35
|
+
{{#eachItem items}}
|
|
36
|
+
{{this.name}}
|
|
37
|
+
{{/eachItem}}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Allowed:**
|
|
41
|
+
|
|
42
|
+
```handlebars
|
|
43
|
+
{{formatDate order.createdAt "yyyy-MM-dd"}}
|
|
44
|
+
|
|
45
|
+
{{#each items}}
|
|
46
|
+
{{this.name}}
|
|
47
|
+
{{/each}}
|
|
48
|
+
|
|
49
|
+
{{! bare binding, not a helper — never flagged }}
|
|
50
|
+
{{firstName}}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## When to Disable
|
|
54
|
+
|
|
55
|
+
Disable only if you rely on a helper that is genuinely available in your MCN account but missing from the bundled catalog (please report it so the catalog can be updated).
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// eslint.config.js
|
|
59
|
+
rules: { 'sfmc/hbs-no-unknown-helper': 'off' }
|
|
60
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `sfmc/hbs-no-unsupported-construct`
|
|
2
|
+
|
|
3
|
+
> Disallow Handlebars constructs unsupported by the Marketing Cloud Next engine (partials, decorators, log).
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `error` in the `-next` configs; `off` in classic configs |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
The Marketing Cloud Next Handlebars engine is a locked-down subset of handlebars.js. Several standard handlebars.js constructs are not available and fail at render time:
|
|
14
|
+
|
|
15
|
+
- Partials — `{{> partialName}}`
|
|
16
|
+
- Partial blocks — `{{#> partialName}}...{{/partialName}}`
|
|
17
|
+
- Inline partials — `{{#*inline "name"}}...{{/inline}}`
|
|
18
|
+
- Decorators — `{{* decorator}}` and `{{#* decorator}}...{{/*decorator}}`
|
|
19
|
+
- The debugging helper `{{log}}`
|
|
20
|
+
|
|
21
|
+
This rule flags each of these, mirroring the `handlebars/unsupported-construct` diagnostic emitted by the language server. The full explanation for the specific construct is included in the reported message.
|
|
22
|
+
|
|
23
|
+
Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
|
|
24
|
+
|
|
25
|
+
## Settings
|
|
26
|
+
|
|
27
|
+
| Setting | Values | Default |
|
|
28
|
+
|---------|--------|---------|
|
|
29
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
|
|
30
|
+
|
|
31
|
+
This rule has no configuration options.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
**Not allowed:**
|
|
36
|
+
|
|
37
|
+
```handlebars
|
|
38
|
+
{{> header}}
|
|
39
|
+
|
|
40
|
+
{{#> layout}}
|
|
41
|
+
body
|
|
42
|
+
{{/layout}}
|
|
43
|
+
|
|
44
|
+
{{log "debugging"}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Allowed:**
|
|
48
|
+
|
|
49
|
+
```handlebars
|
|
50
|
+
{{! inline the shared markup instead of using a partial }}
|
|
51
|
+
<h1>{{title}}</h1>
|
|
52
|
+
|
|
53
|
+
{{#each items}}
|
|
54
|
+
{{this.name}}
|
|
55
|
+
{{/each}}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## When to Disable
|
|
59
|
+
|
|
60
|
+
These constructs do not run in MCN, so disabling the rule only hides a guaranteed render-time failure. Leave it enabled.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
// eslint.config.js
|
|
64
|
+
rules: { 'sfmc/hbs-no-unsupported-construct': 'off' }
|
|
65
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "ESLint plugin for Salesforce Marketing Cloud  AMPscript and Server-Side JavaScript (SSJS)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"sfmc"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"ampscript-
|
|
37
|
-
"
|
|
35
|
+
"@handlebars/parser": "^2.2.2",
|
|
36
|
+
"ampscript-data": "^3.0.0",
|
|
37
|
+
"ampscript-parser": "^0.2.0",
|
|
38
|
+
"handlebars-data": "^0.3.0",
|
|
39
|
+
"ssjs-data": "^0.14.0"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
42
|
"eslint": ">=9.0.0"
|
|
@@ -52,19 +54,19 @@
|
|
|
52
54
|
},
|
|
53
55
|
"license": "MIT",
|
|
54
56
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
57
|
+
"node": ">=22.0.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@eslint/js": "^10.0.1",
|
|
59
|
-
"eslint": "^10.
|
|
61
|
+
"eslint": "^10.6.0",
|
|
60
62
|
"eslint-config-prettier": "^10.1.8",
|
|
61
|
-
"eslint-plugin-jsdoc": "^
|
|
62
|
-
"eslint-plugin-prettier": "^5.5.
|
|
63
|
-
"eslint-plugin-unicorn": "^
|
|
64
|
-
"globals": "^17.
|
|
63
|
+
"eslint-plugin-jsdoc": "^63.0.10",
|
|
64
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
65
|
+
"eslint-plugin-unicorn": "^70.0.0",
|
|
66
|
+
"globals": "^17.7.0",
|
|
65
67
|
"husky": "^9.1.7",
|
|
66
|
-
"lint-staged": "^17.0.
|
|
67
|
-
"prettier": "^3.
|
|
68
|
+
"lint-staged": "^17.0.8",
|
|
69
|
+
"prettier": "^3.9.4"
|
|
68
70
|
},
|
|
69
71
|
"lint-staged": {
|
|
70
72
|
"*.{js,mjs,cjs}": [
|
package/src/ampscript-parser.js
CHANGED
|
@@ -91,7 +91,7 @@ function annotateNode(node, lineStarts, comments) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// Rename colliding ESTree types to Amp-prefixed equivalents
|
|
94
|
-
if (node.type && NODE_RENAME
|
|
94
|
+
if (node.type && Object.hasOwn(NODE_RENAME, node.type)) {
|
|
95
95
|
node.type = NODE_RENAME[node.type];
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -24,6 +24,22 @@ function countNewlinesBefore(text, pos) {
|
|
|
24
24
|
return count;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Scans forward for the closing `=%%` of an inline AMPscript expression.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} text - Full document text.
|
|
31
|
+
* @param {number} from - Index to start scanning from (just past the opening `%%=`).
|
|
32
|
+
* @returns {number} Index just past the closing `=%%`, or -1 when not found.
|
|
33
|
+
*/
|
|
34
|
+
function findInlineExpressionEnd(text, from) {
|
|
35
|
+
for (let index = from; index < text.length; index++) {
|
|
36
|
+
if (text[index] === '=' && text[index + 1] === '%' && text[index + 2] === '%') {
|
|
37
|
+
return index + 3;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export function preprocess(text, filename) {
|
|
28
44
|
const blocks = [];
|
|
29
45
|
let index = 0;
|
|
@@ -91,28 +107,18 @@ export function preprocess(text, filename) {
|
|
|
91
107
|
|
|
92
108
|
// %%= ... =%%
|
|
93
109
|
if (text[index] === '%' && text[index + 1] === '%' && text[index + 2] === '=') {
|
|
94
|
-
const
|
|
95
|
-
index
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
while (index < text.length) {
|
|
99
|
-
if (text[index] === '=' && text[index + 1] === '%' && text[index + 2] === '%') {
|
|
100
|
-
index += 3;
|
|
101
|
-
found = true;
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
index++;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!found) {
|
|
110
|
+
const expressionStart = index;
|
|
111
|
+
const expressionEnd = findInlineExpressionEnd(text, index + 3);
|
|
112
|
+
if (expressionEnd === -1) {
|
|
108
113
|
continue;
|
|
109
114
|
}
|
|
115
|
+
index = expressionEnd;
|
|
110
116
|
|
|
111
117
|
// Wrap in %%[ ]%% so the AMPscript parser receives valid block syntax.
|
|
112
118
|
// %%[ is 3 chars, same as %%= — column offsets for inner code are preserved.
|
|
113
|
-
const innerCode = text.slice(
|
|
119
|
+
const innerCode = text.slice(expressionStart + 3, index - 3);
|
|
114
120
|
const wrappedBlock = `%%[${innerCode}]%%`;
|
|
115
|
-
const padding = '\n'.repeat(countNewlinesBefore(text,
|
|
121
|
+
const padding = '\n'.repeat(countNewlinesBefore(text, expressionStart));
|
|
116
122
|
|
|
117
123
|
blocks.push({
|
|
118
124
|
text: padding + wrappedBlock,
|