prettier-plugin-sfmc 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # prettier-plugin-sfmc
2
2
 
3
- Unified Prettier plugin for **Salesforce Marketing Cloud** — formats **AMPscript** (`.ampscript`, `.amp`, `.html`) and registers **SSJS** (`.ssjs`) files with Prettier's built-in JavaScript formatter.
3
+ Unified Prettier plugin for **Salesforce Marketing Cloud** — formats **AMPscript** (`.ampscript`, `.amp`, `.html`), registers **SSJS** (`.ssjs`) with Prettiers JavaScript formatter, and formats **SQL** (`.sql`) via embedded [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql).
4
4
 
5
5
  ## Installation
6
6
 
@@ -12,7 +12,7 @@ Requires Prettier 3.7+.
12
12
 
13
13
  ## Quick Start
14
14
 
15
- Prettier auto-discovers plugins installed in `node_modules`. No config needed for `.ampscript`, `.amp`, and `.ssjs` files.
15
+ Prettier auto-discovers plugins installed in `node_modules`. No config needed for `.ampscript`, `.amp`, `.ssjs`, and `.sql` files.
16
16
 
17
17
  For `.html` files containing AMPscript, the plugin's parser handles AMPscript regions while delegating HTML to Prettier's built-in HTML formatter. SSJS `<script runat="server">` blocks are formatted as JavaScript by Prettier's HTML pipeline automatically.
18
18
 
@@ -20,7 +20,7 @@ To use explicitly in `.prettierrc`:
20
20
 
21
21
  ```json
22
22
  {
23
- "plugins": ["prettier-plugin-sfmc"]
23
+ "plugins": ["prettier-plugin-sfmc"]
24
24
  }
25
25
  ```
26
26
 
@@ -28,25 +28,25 @@ To use explicitly in `.prettierrc`:
28
28
 
29
29
  All options use the `ampscript` prefix — they control AMPscript formatting behavior.
30
30
 
31
- | Option | Type | Default | Description |
32
- | ------------------------------------ | ---------------------------------------------------------------------------- | --------------- | ---------------------------------------------- |
33
- | [`ampscriptSpacing`](docs/options/ampscript-spacing.md) | boolean | `true` | Spacing in inline expressions: `%%= V(@x) =%%` |
34
- | [`ampscriptEnforceVariableCasing`](docs/options/ampscript-enforce-variable-casing.md) | boolean | `true` | Normalize variable casing to first occurrence |
31
+ | Option | Type | Default | Description |
32
+ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------- | ---------------------------------------------- |
33
+ | [`ampscriptSpacing`](docs/options/ampscript-spacing.md) | boolean | `true` | Spacing in inline expressions: `%%= V(@x) =%%` |
34
+ | [`ampscriptEnforceVariableCasing`](docs/options/ampscript-enforce-variable-casing.md) | boolean | `true` | Normalize variable casing to first occurrence |
35
35
  | [`ampscriptRemoveUnnecessaryBrackets`](docs/options/ampscript-remove-unnecessary-brackets.md) | boolean | `true` | Remove needless parentheses |
36
- | [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md) | `"single"` \| `"double"` | `"single"` | String quote style |
37
- | [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md) | `"lower"` \| `"upper"` \| `"preserve"` | `"lower"` | Keyword casing |
38
- | [`ampscriptFunctionCase`](docs/options/ampscript-function-case.md) | `"upper-camel"` \| `"lower-camel"` \| `"upper"` \| `"lower"` \| `"preserve"` | `"upper-camel"` | Function name casing |
39
- | [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md) | boolean | `true` | Line breaks before/after `%%[ ]%%` blocks |
40
- | [`ampscriptVarDeclarationStyle`](docs/options/ampscript-var-declaration-style.md) | `"auto"` \| `"single-line"` \| `"multi-line"` | `"auto"` | Var declaration formatting |
36
+ | [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md) | `"single"` \| `"double"` | `"single"` | String quote style |
37
+ | [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md) | `"lower"` \| `"upper"` \| `"preserve"` | `"lower"` | Keyword casing |
38
+ | [`ampscriptFunctionCase`](docs/options/ampscript-function-case.md) | `"upper-camel"` \| `"lower-camel"` \| `"upper"` \| `"lower"` \| `"preserve"` | `"upper-camel"` | Function name casing |
39
+ | [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md) | boolean | `true` | Line breaks before/after `%%[ ]%%` blocks |
40
+ | [`ampscriptVarDeclarationStyle`](docs/options/ampscript-var-declaration-style.md) | `"auto"` \| `"single-line"` \| `"multi-line"` | `"multi-line"` | Var declaration formatting |
41
41
 
42
42
  ### Example `.prettierrc`
43
43
 
44
44
  ```json
45
45
  {
46
- "plugins": ["prettier-plugin-sfmc"],
47
- "ampscriptKeywordCase": "upper",
48
- "ampscriptFunctionCase": "upper-camel",
49
- "ampscriptQuoteStyle": "single"
46
+ "plugins": ["prettier-plugin-sfmc"],
47
+ "ampscriptKeywordCase": "upper",
48
+ "ampscriptFunctionCase": "upper-camel",
49
+ "ampscriptQuoteStyle": "single"
50
50
  }
51
51
  ```
52
52
 
@@ -58,6 +58,45 @@ All options use the `ampscript` prefix — they control AMPscript formatting beh
58
58
  | `.amp` | `ampscript-parse` | Full AMPscript formatting |
59
59
  | `.html` | `ampscript-parse` | AMPscript formatted; HTML delegated to Prettier |
60
60
  | `.ssjs` | `babel` (built-in) | Standard JavaScript formatting |
61
+ | `.sql` | `sql` | SQL via composed `prettier-plugin-sql` |
62
+
63
+ ## Core Prettier defaults (AMPscript, HTML, SQL)
64
+
65
+ This plugin exports [Prettier `defaultOptions`](https://prettier.io/docs/plugins#defaultoptions). Prettier merges them when **this plugin’s printer** is active (AMPscript / embedded AMPscript in HTML, and SQL via the composed `sql` printer). They affect layout choices such as `tabWidth` and `printWidth` for those formats.
66
+
67
+ | Option | Default | Rationale |
68
+ | ------ | ------- | --------- |
69
+ | `useTabs` | `false` | SFMC often normalizes tabs away on save; spaces keep layout stable. |
70
+ | `tabWidth` | `4` | Readable indentation (override in config if you prefer 2). |
71
+ | `printWidth` | `100` | Fits typical editor panes better than Prettier’s 80. |
72
+ | `singleQuote` | `true` | Matches common JS style and `ampscriptQuoteStyle: 'single'` for AMPscript-related output where the core quote option applies. |
73
+ | `trailingComma` | `'none'` | Avoids trailing commas in stacks where they can be problematic (notably SSJS — see below). |
74
+
75
+ String delimiters inside AMPscript blocks still follow `ampscriptQuoteStyle`. See [Prettier options](https://prettier.io/docs/options) for every standard flag.
76
+
77
+ ### SSJS (`.ssjs`)
78
+
79
+ `.ssjs` files use Prettier’s built-in **JavaScript** printer, not this plugin’s printer, so the `defaultOptions` above are **not** applied automatically to SSJS. Use `.prettierrc` (or [overrides](https://prettier.io/docs/configuration#configuration-overrides) on `*.ssjs`) if you want the same style, for example:
80
+
81
+ ```json
82
+ {
83
+ "plugins": ["prettier-plugin-sfmc"],
84
+ "singleQuote": true,
85
+ "trailingComma": "none",
86
+ "tabWidth": 4,
87
+ "printWidth": 100
88
+ }
89
+ ```
90
+
91
+ To keep **AMPscript-only** indentation at 2 spaces while using different values for SSJS, use `overrides` keyed by file pattern.
92
+
93
+ ## SQL (Transact-SQL / SFMC)
94
+
95
+ `.sql` formatting is provided by [prettier-plugin-sql](https://github.com/un-ts/prettier/tree/master/packages/sql) (npm: [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql)). You only need `prettier` and `prettier-plugin-sfmc`; do not add a second entry in `plugins` for SQL.
96
+
97
+ **Defaults chosen for SFMC-style T-SQL:** `language` `tsql`, `formatter` `sql-formatter`, `keywordCase` / `functionCase` `upper`, `identifierCase` / `dataTypeCase` `preserve`. Override any of these in `.prettierrc` or under `overrides` with `files: "*.sql"` if needed.
98
+
99
+ Core layout still follows [Prettier options](https://prettier.io/docs/options). SQL-specific knobs (`expressionWidth`, `linesBetweenQueries`, etc.) are documented in the upstream package README.
61
100
 
62
101
  ## HTML Embedding
63
102
 
@@ -69,13 +108,67 @@ When formatting `.html` files, the plugin:
69
108
 
70
109
  This means a single plugin handles all SFMC formatting in HTML email templates.
71
110
 
72
- ## Migration from `prettier-plugin-ampscript` / `prettier-plugin-ssjs`
111
+ ## Ignoring Code
112
+
113
+ Prettier provides two ways to exclude code from formatting: ignore comments for specific code sections, and `.prettierignore` for entire files.
114
+
115
+ ### SSJS
73
116
 
74
- Both plugins have been merged into this single package:
117
+ SSJS uses Prettier's built-in JavaScript formatter. Use `// prettier-ignore` to exclude the next statement from formatting:
75
118
 
76
- 1. Replace `prettier-plugin-ampscript` and `prettier-plugin-ssjs` with `prettier-plugin-sfmc`
77
- 2. Update the `plugins` array in `.prettierrc` if explicitly configured
78
- 3. All `ampscript*` option names are unchanged
119
+ ```js
120
+ // prettier-ignore
121
+ var config = {
122
+ clientId: "abc123",
123
+ clientSecret: "xyz789",
124
+ endpoint: "https://example.com"
125
+ };
126
+ ```
127
+
128
+ Without the comment, Prettier would collapse the aligned spacing.
129
+
130
+ ### AMPscript
131
+
132
+ AMPscript uses block comment syntax. Use `/* prettier-ignore */` to exclude the next statement from formatting:
133
+
134
+ ```ampscript
135
+ %%[
136
+ /* prettier-ignore */
137
+ set @matrix = Concat(
138
+ '1, 0, 0,',
139
+ '0, 1, 0,',
140
+ '0, 0, 1'
141
+ )
142
+ ]%%
143
+ ```
144
+
145
+ ### Ignoring Files: .prettierignore
146
+
147
+ To exclude entire files from formatting, create a `.prettierignore` file in the root of your project. It uses [gitignore syntax](https://git-scm.com/docs/gitignore#_pattern_format).
148
+
149
+ Example:
150
+
151
+ ```text
152
+ # Ignore artifacts:
153
+ build
154
+ coverage
155
+
156
+ # Ignore all HTML files:
157
+ **/*.html
158
+ ```
159
+
160
+ By default Prettier ignores files in version control directories (`.git`, `.svn`, `.hg`) and `node_modules`. Prettier also follows rules in `.gitignore` if present.
161
+
162
+ #### SMS and Mobile Messages
163
+
164
+ For SMS or MobilePush messages, line breaks and exact character placement often matter for delivery and display. Excluding these files from formatting prevents Prettier from altering whitespace that affects message rendering.
165
+
166
+ Example pattern for SFMC DevTools mobile message assets:
167
+
168
+ ```text
169
+ # Preserve exact formatting in mobile messages
170
+ **/*.asset-mobile-meta.amp
171
+ ```
79
172
 
80
173
  ## License
81
174
 
@@ -44,6 +44,30 @@ Normalises the casing of every known AMPscript function name. AMPscript function
44
44
  ]%%
45
45
  ```
46
46
 
47
+ ### `"lower-camel"`
48
+
49
+ Uses the same canonical name as `"upper-camel"`, but with the first character lowercased (camelCase).
50
+
51
+ **Input:**
52
+
53
+ ```ampscript
54
+ %%[
55
+ var @result
56
+ set @result = LOOKUP("MyDE", "Value", "Key", @key)
57
+ set @html = contentblockbykey("my-block")
58
+ ]%%
59
+ ```
60
+
61
+ **Output:**
62
+
63
+ ```ampscript
64
+ %%[
65
+ var @result
66
+ set @result = lookup("MyDE", "Value", "Key", @key)
67
+ set @html = contentBlockByKey("my-block")
68
+ ]%%
69
+ ```
70
+
47
71
  ### `"upper"`
48
72
 
49
73
  **Output:**
@@ -5,22 +5,22 @@
5
5
  | | |
6
6
  |---|---|
7
7
  | **Type** | `"auto"` \| `"single-line"` \| `"multi-line"` |
8
- | **Default** | `"auto"` |
8
+ | **Default** | `"multi-line"` |
9
9
  | **Applied by** | `prettier --write` · VS Code format-on-save (requires the [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) |
10
10
 
11
11
  ## What It Controls
12
12
 
13
- Determines whether multiple variables in a single `var` statement are kept on one line, split to one variable per line, or left to Prettier's `printWidth` heuristic. AMPscript allows both `var @a, @b, @c` (all on one line) and separate `var` statements for each variable.
13
+ Determines whether multiple variables in a single `var` statement stay on one line, break after each comma (still a single `var` keyword), or follow Prettiers `printWidth` heuristic. AMPscript allows `var @a, @b, @c` on one line; this option does not split into separate `var` statements.
14
14
 
15
15
  ## Settings
16
16
 
17
17
  | Value | Effect |
18
18
  |-------|--------|
19
- | `"auto"` (default) | Single line when it fits within `printWidth`; one-per-line when it doesn't |
19
+ | `"multi-line"` (default) | One `var` keyword; variables separated by comma and line breaks (indented under `var`) |
20
+ | `"auto"` | Single line when it fits within `printWidth`; otherwise same comma-and-line-break layout as `"multi-line"` |
20
21
  | `"single-line"` | Always `var @a, @b, @c` on one line regardless of width |
21
- | `"multi-line"` | Always one variable per `var` statement |
22
22
 
23
- ### `"auto"` (default)
23
+ ### `"multi-line"` (default)
24
24
 
25
25
  **Input:**
26
26
 
@@ -30,49 +30,48 @@ Determines whether multiple variables in a single `var` statement are kept on on
30
30
  ]%%
31
31
  ```
32
32
 
33
- **Output (when line is short enough for `printWidth`):**
33
+ **Output** (indentation follows your Prettier `tabWidth`; shown with the plugin default of 4):
34
34
 
35
35
  ```ampscript
36
36
  %%[
37
- var @firstName, @lastName, @email
37
+ var @firstName,
38
+ @lastName,
39
+ @email,
40
+ @address,
41
+ @city,
42
+ @postalCode
38
43
  ]%%
39
44
  ```
40
45
 
41
- **Output (when line exceeds `printWidth`):**
46
+ ### `"auto"`
47
+
48
+ **Input:**
42
49
 
43
50
  ```ampscript
44
51
  %%[
45
- var @firstName
46
- var @lastName
47
- var @email
48
- var @address
49
- var @city
50
- var @postalCode
52
+ var @firstName, @lastName, @email
51
53
  ]%%
52
54
  ```
53
55
 
54
- ### `"single-line"`
55
-
56
- **Output:**
56
+ **Output (when the line fits within `printWidth`):**
57
57
 
58
58
  ```ampscript
59
59
  %%[
60
- var @firstName, @lastName, @email, @address, @city, @postalCode
60
+ var @firstName, @lastName, @email
61
61
  ]%%
62
62
  ```
63
63
 
64
- ### `"multi-line"`
64
+ **Output (when the line exceeds `printWidth`):**
65
+
66
+ Same wrapped shape as `"multi-line"`: one `var`, commas at end of lines, continuation lines indented.
67
+
68
+ ### `"single-line"`
65
69
 
66
70
  **Output:**
67
71
 
68
72
  ```ampscript
69
73
  %%[
70
- var @firstName
71
- var @lastName
72
- var @email
73
- var @address
74
- var @city
75
- var @postalCode
74
+ var @firstName, @lastName, @email, @address, @city, @postalCode
76
75
  ]%%
77
76
  ```
78
77
 
@@ -81,6 +80,6 @@ Determines whether multiple variables in a single `var` statement are kept on on
81
80
  ```json
82
81
  {
83
82
  "plugins": ["prettier-plugin-sfmc"],
84
- "ampscriptVarDeclarationStyle": "multi-line"
83
+ "ampscriptVarDeclarationStyle": "auto"
85
84
  }
86
85
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prettier-plugin-sfmc",
3
- "version": "0.1.0",
4
- "description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript formatting and SSJS file registration",
3
+ "version": "0.2.0",
4
+ "description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript, SSJS, and SQL formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -24,12 +24,19 @@
24
24
  "author": "Joern Berkefeld",
25
25
  "scripts": {
26
26
  "example": "prettier --plugin . example.ampscript",
27
- "test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js"
27
+ "test": "node ./scripts/run-jest.mjs",
28
+ "lint": "eslint .",
29
+ "lint:fix": "eslint --fix .",
30
+ "format": "prettier --write .",
31
+ "format:check": "prettier --check ."
28
32
  },
29
33
  "keywords": [
30
34
  "prettier",
31
35
  "ampscript",
32
36
  "ssjs",
37
+ "sql",
38
+ "tsql",
39
+ "transact-sql",
33
40
  "sfmc",
34
41
  "salesforce",
35
42
  "marketing-cloud",
@@ -38,14 +45,22 @@
38
45
  ],
39
46
  "license": "MIT",
40
47
  "dependencies": {
41
- "ampscript-data": "^0.1.1",
42
- "ampscript-parser": "^0.1.0",
43
- "ssjs-data": "^0.1.0"
48
+ "ampscript-data": "^0.1.3",
49
+ "ampscript-parser": "^0.1.1",
50
+ "prettier-plugin-sql": "^0.20.0",
51
+ "ssjs-data": "^0.2.2"
44
52
  },
45
53
  "devDependencies": {
46
- "jest": "^29.0.0",
54
+ "@eslint/js": "^10.0.1",
47
55
  "@jest/globals": "^29.0.0",
48
- "prettier": "^3.7.0"
56
+ "eslint": "^10.1.0",
57
+ "eslint-config-prettier": "^10.1.8",
58
+ "eslint-plugin-jsdoc": "^62.0.0",
59
+ "eslint-plugin-prettier": "^5.5.0",
60
+ "eslint-plugin-unicorn": "^64.0.0",
61
+ "globals": "^17.4.0",
62
+ "jest": "^29.0.0",
63
+ "prettier": "^3.8.1"
49
64
  },
50
65
  "peerDependencies": {
51
66
  "prettier": ">=3.7.0"
package/src/index.js CHANGED
@@ -2,17 +2,45 @@
2
2
  * prettier-plugin-sfmc
3
3
  *
4
4
  * A unified Prettier plugin for Salesforce Marketing Cloud.
5
- * Handles AMPscript formatting (.ampscript, .amp, .html) and
6
- * SSJS file registration (.ssjs).
5
+ * Handles AMPscript formatting (.ampscript, .amp, .html), SSJS file
6
+ * registration (.ssjs), and SQL (.sql) via composed prettier-plugin-sql.
7
7
  *
8
8
  * Exports: languages, parsers, printers, options, defaultOptions
9
9
  * Following the Prettier v3 plugin API (ESM).
10
10
  */
11
11
 
12
12
  import { parse } from 'ampscript-parser';
13
+ import sqlPlugin from 'prettier-plugin-sql';
13
14
  import { printAmpscriptNode, collectVariableMap } from './printer.js';
14
15
  import * as prettier from 'prettier';
15
16
 
17
+ /**
18
+ * Clone sql plugin option descriptors and set SFMC-friendly defaults.
19
+ *
20
+ * @param {Record<string, object>} sqlOptions
21
+ * @returns {Record<string, object>} Option map with adjusted defaults.
22
+ */
23
+ function mergeSqlOptionsWithSfmcDefaults(sqlOptions) {
24
+ /** @type {Record<string, object>} */
25
+ const merged = {};
26
+ for (const [key, descriptor] of Object.entries(sqlOptions)) {
27
+ merged[key] = { ...descriptor };
28
+ }
29
+ const defaults = {
30
+ language: 'tsql',
31
+ keywordCase: 'upper',
32
+ functionCase: 'upper',
33
+ identifierCase: 'preserve',
34
+ dataTypeCase: 'preserve',
35
+ };
36
+ for (const [key, defaultValue] of Object.entries(defaults)) {
37
+ if (merged[key]) {
38
+ merged[key].default = defaultValue;
39
+ }
40
+ }
41
+ return merged;
42
+ }
43
+
16
44
  // ── Languages ────────────────────────────────────────────────────────────────
17
45
 
18
46
  export const languages = [
@@ -28,6 +56,7 @@ export const languages = [
28
56
  extensions: ['.ssjs'],
29
57
  vscodeLanguageIds: ['ssjs'],
30
58
  },
59
+ ...sqlPlugin.languages,
31
60
  ];
32
61
 
33
62
  // ── Parsers ──────────────────────────────────────────────────────────────────
@@ -35,6 +64,7 @@ export const languages = [
35
64
  const PRAGMA_RE = /^\s*\/\*\*?\s*@(?:format|prettier)\s*\*\//;
36
65
 
37
66
  export const parsers = {
67
+ ...sqlPlugin.parsers,
38
68
  'ampscript-parse': {
39
69
  parse(text) {
40
70
  return parse(text);
@@ -52,7 +82,9 @@ export const parsers = {
52
82
  const blockClose = text.indexOf(']%%', blockOpen);
53
83
  if (blockClose !== -1) {
54
84
  const blockContent = text.slice(blockOpen + 3, blockClose);
55
- if (PRAGMA_RE.test(blockContent)) return true;
85
+ if (PRAGMA_RE.test(blockContent)) {
86
+ return true;
87
+ }
56
88
  }
57
89
  }
58
90
  return PRAGMA_RE.test(text);
@@ -63,6 +95,7 @@ export const parsers = {
63
95
  // ── Printers ─────────────────────────────────────────────────────────────────
64
96
 
65
97
  export const printers = {
98
+ ...sqlPlugin.printers,
66
99
  'ampscript-ast': {
67
100
  print(path, options, print) {
68
101
  const node = path.node;
@@ -76,12 +109,16 @@ export const printers = {
76
109
 
77
110
  embed(path, options) {
78
111
  const node = path.node;
79
- if (node.type !== 'Document') return;
112
+ if (node.type !== 'Document') {
113
+ return;
114
+ }
80
115
 
81
116
  const hasHtml = node.children.some(
82
117
  (c) => c.type === 'Content' && /<[a-zA-Z!/]/.test(c.value),
83
118
  );
84
- if (!hasHtml) return;
119
+ if (!hasHtml) {
120
+ return;
121
+ }
85
122
 
86
123
  return async (textToDocument, print) => {
87
124
  if (!options.__ampscriptVariableMap) {
@@ -111,19 +148,27 @@ export const printers = {
111
148
 
112
149
  const re = new RegExp(String.raw`${PH}(\d+)END`, 'g');
113
150
  return prettier.doc.utils.mapDoc(htmlDocument, (d) => {
114
- if (typeof d !== 'string') return d;
115
- if (!d.includes(PH)) return d;
151
+ if (typeof d !== 'string') {
152
+ return d;
153
+ }
154
+ if (!d.includes(PH)) {
155
+ return d;
156
+ }
116
157
 
117
158
  const parts = [];
118
159
  let last = 0;
119
160
  let m;
120
161
  re.lastIndex = 0;
121
162
  while ((m = re.exec(d)) !== null) {
122
- if (m.index > last) parts.push(d.slice(last, m.index));
163
+ if (m.index > last) {
164
+ parts.push(d.slice(last, m.index));
165
+ }
123
166
  parts.push(print(['children', Number.parseInt(m[1], 10)]));
124
167
  last = m.index + m[0].length;
125
168
  }
126
- if (last < d.length) parts.push(d.slice(last));
169
+ if (last < d.length) {
170
+ parts.push(d.slice(last));
171
+ }
127
172
  return parts.length === 1 ? parts[0] : parts;
128
173
  });
129
174
  };
@@ -143,7 +188,9 @@ export const printers = {
143
188
 
144
189
  printComment(path) {
145
190
  const comment = path.node;
146
- if (comment.type === 'Comment') return comment.value;
191
+ if (comment.type === 'Comment') {
192
+ return comment.value;
193
+ }
147
194
  return '';
148
195
  },
149
196
 
@@ -205,7 +252,7 @@ export const printers = {
205
252
 
206
253
  // ── Options ──────────────────────────────────────────────────────────────────
207
254
 
208
- export const options = {
255
+ const sfmcOptions = {
209
256
  ampscriptSpacing: {
210
257
  type: 'boolean',
211
258
  category: 'AMPscript',
@@ -292,12 +339,12 @@ export const options = {
292
339
  ampscriptVarDeclarationStyle: {
293
340
  type: 'choice',
294
341
  category: 'AMPscript',
295
- default: 'auto',
342
+ default: 'multi-line',
296
343
  choices: [
297
344
  {
298
345
  value: 'auto',
299
346
  description:
300
- "Let Prettier decide based on printWidth: single line when it fits, one-per-line when it doesn't.",
347
+ 'Let Prettier decide based on printWidth: single line when it fits, otherwise break after each comma.',
301
348
  },
302
349
  {
303
350
  value: 'single-line',
@@ -305,16 +352,25 @@ export const options = {
305
352
  },
306
353
  {
307
354
  value: 'multi-line',
308
- description: 'Always place each variable on its own line.',
355
+ description:
356
+ 'Always break after each comma: one `var` keyword, variables listed on separate lines.',
309
357
  },
310
358
  ],
311
359
  description: 'Control how var declarations with multiple variables are formatted.',
312
360
  },
313
361
  };
314
362
 
363
+ export const options = {
364
+ ...mergeSqlOptionsWithSfmcDefaults(sqlPlugin.options),
365
+ ...sfmcOptions,
366
+ };
367
+
315
368
  // ── Default Options ──────────────────────────────────────────────────────────
316
369
 
317
370
  export const defaultOptions = {
318
- tabWidth: 2,
319
371
  useTabs: false,
372
+ tabWidth: 4,
373
+ printWidth: 100,
374
+ singleQuote: true,
375
+ trailingComma: 'none',
320
376
  };
package/src/printer.js CHANGED
@@ -13,12 +13,16 @@ const { group, indent, join, line, softline, hardline } = prettier.doc.builders;
13
13
 
14
14
  /**
15
15
  * Collect all variable names in first-occurrence order for consistent casing.
16
+ *
17
+ * @param node
16
18
  */
17
19
  function collectVariableMap(node) {
18
20
  const map = new Map(); // lowercase -> first-seen casing
19
21
 
20
22
  function walk(n) {
21
- if (!n || typeof n !== 'object') return;
23
+ if (!n || typeof n !== 'object') {
24
+ return;
25
+ }
22
26
 
23
27
  if (n.type === 'Variable' && n.value) {
24
28
  const lower = n.value.toLowerCase();
@@ -27,28 +31,64 @@ function collectVariableMap(node) {
27
31
  }
28
32
  }
29
33
 
30
- if (Array.isArray(n.children)) n.children.forEach(walk);
31
- if (Array.isArray(n.statements)) n.statements.forEach(walk);
32
- if (n.target) walk(n.target);
33
- if (n.value && typeof n.value === 'object') walk(n.value);
34
- if (n.expression) walk(n.expression);
35
- if (n.condition) walk(n.condition);
36
- if (n.left) walk(n.left);
37
- if (n.right) walk(n.right);
38
- if (n.argument) walk(n.argument);
39
- if (Array.isArray(n.arguments)) n.arguments.forEach(walk);
40
- if (Array.isArray(n.variables)) n.variables.forEach(walk);
41
- if (Array.isArray(n.consequent)) n.consequent.forEach(walk);
34
+ if (Array.isArray(n.children)) {
35
+ n.children.forEach(walk);
36
+ }
37
+ if (Array.isArray(n.statements)) {
38
+ n.statements.forEach(walk);
39
+ }
40
+ if (n.target) {
41
+ walk(n.target);
42
+ }
43
+ if (n.value && typeof n.value === 'object') {
44
+ walk(n.value);
45
+ }
46
+ if (n.expression) {
47
+ walk(n.expression);
48
+ }
49
+ if (n.condition) {
50
+ walk(n.condition);
51
+ }
52
+ if (n.left) {
53
+ walk(n.left);
54
+ }
55
+ if (n.right) {
56
+ walk(n.right);
57
+ }
58
+ if (n.argument) {
59
+ walk(n.argument);
60
+ }
61
+ if (Array.isArray(n.arguments)) {
62
+ n.arguments.forEach(walk);
63
+ }
64
+ if (Array.isArray(n.variables)) {
65
+ n.variables.forEach(walk);
66
+ }
67
+ if (Array.isArray(n.consequent)) {
68
+ n.consequent.forEach(walk);
69
+ }
42
70
  if (Array.isArray(n.alternates)) {
43
71
  for (const alt of n.alternates) {
44
- if (alt.condition) walk(alt.condition);
45
- if (Array.isArray(alt.body)) alt.body.forEach(walk);
72
+ if (alt.condition) {
73
+ walk(alt.condition);
74
+ }
75
+ if (Array.isArray(alt.body)) {
76
+ alt.body.forEach(walk);
77
+ }
46
78
  }
47
79
  }
48
- if (Array.isArray(n.body)) n.body.forEach(walk);
49
- if (n.counter) walk(n.counter);
50
- if (n.startExpr) walk(n.startExpr);
51
- if (n.endExpr) walk(n.endExpr);
80
+ if (Array.isArray(n.body)) {
81
+ n.body.forEach(walk);
82
+ }
83
+ if (n.counter) {
84
+ walk(n.counter);
85
+ }
86
+ if (n.startExpr) {
87
+ walk(n.startExpr);
88
+ }
89
+ if (n.endExpr) {
90
+ walk(n.endExpr);
91
+ }
52
92
  }
53
93
 
54
94
  walk(node);
@@ -57,9 +97,13 @@ function collectVariableMap(node) {
57
97
 
58
98
  /**
59
99
  * Detect needless parentheses around a single variable or literal.
100
+ *
101
+ * @param node
60
102
  */
61
103
  function isNeedlessParenExpression(node) {
62
- if (node.type !== 'ParenExpression') return false;
104
+ if (node.type !== 'ParenExpression') {
105
+ return false;
106
+ }
63
107
  const inner = node.expression;
64
108
  return (
65
109
  inner.type === 'Variable' ||
@@ -90,18 +134,25 @@ function getOperatorPrecedence(op) {
90
134
  * Context-aware check for whether a ParenExpression's parens can be removed.
91
135
  * Covers simple atoms, function calls, nested parens, statement-level
92
136
  * positions, and precedence-based removal inside binary expressions.
137
+ *
138
+ * @param path
139
+ * @param node
93
140
  */
94
141
  function canRemoveParens(path, node) {
95
142
  const inner = node.expression;
96
143
 
97
- if (isNeedlessParenExpression(node)) return true;
144
+ if (isNeedlessParenExpression(node)) {
145
+ return true;
146
+ }
98
147
 
99
148
  if (inner.type === 'FunctionCall' || inner.type === 'ParenExpression') {
100
149
  return true;
101
150
  }
102
151
 
103
152
  const parent = path.getParentNode();
104
- if (!parent) return false;
153
+ if (!parent) {
154
+ return false;
155
+ }
105
156
 
106
157
  const STATEMENT_PARENTS = [
107
158
  'SetStatement',
@@ -111,7 +162,9 @@ function canRemoveParens(path, node) {
111
162
  'ForStatement',
112
163
  'InlineExpression',
113
164
  ];
114
- if (STATEMENT_PARENTS.includes(parent.type)) return true;
165
+ if (STATEMENT_PARENTS.includes(parent.type)) {
166
+ return true;
167
+ }
115
168
 
116
169
  if (parent.type === 'BinaryExpression') {
117
170
  const parentPrec = getOperatorPrecedence(parent.operator);
@@ -133,41 +186,64 @@ function canRemoveParens(path, node) {
133
186
  * processing are applied to every child node.
134
187
  *
135
188
  * @param {import("prettier").AstPath} path
136
- * @param {object} options Merged Prettier + plugin options
137
- * @param {function} print Prettier recursive print callback
189
+ * @param {object} options Merged Prettier + plugin options
190
+ * @param {function} print Prettier recursive print callback
138
191
  */
139
192
  function printAmpscriptNode(path, options, print) {
140
193
  const node = path.node;
141
- if (!node) return '';
194
+ if (!node) {
195
+ return '';
196
+ }
142
197
 
143
198
  const variableMap = options.__ampscriptVariableMap || new Map();
144
199
  const enforceSpacing = options.ampscriptSpacing;
145
200
  const quoteChar = options.ampscriptQuoteStyle === 'single' ? "'" : '"';
146
201
  const enforceCasing = options.ampscriptEnforceVariableCasing;
147
202
  const removeBrackets = options.ampscriptRemoveUnnecessaryBrackets;
148
- const variableStyle = options.ampscriptVarDeclarationStyle || 'auto';
203
+ const variableStyle = options.ampscriptVarDeclarationStyle || 'multi-line';
149
204
  const keywordCase = options.ampscriptKeywordCase || 'lower';
150
205
  const functionCase = options.ampscriptFunctionCase || 'upper-camel';
151
206
  const blockLineBreaks = options.ampscriptBlockLineBreaks !== false;
152
207
 
153
208
  function resolveVariable(variableName) {
154
- if (!enforceCasing) return variableName;
209
+ if (!enforceCasing) {
210
+ return variableName;
211
+ }
155
212
  const lower = variableName.toLowerCase();
156
213
  return variableMap.get(lower) || variableName;
157
214
  }
158
215
 
159
- /** Apply keyword casing rule. Pass originalText for preserve mode. */
216
+ /**
217
+ * Apply keyword casing rule. Pass originalText for preserve mode.
218
+ *
219
+ * @param keyword
220
+ * @param originalText
221
+ */
160
222
  function kw(keyword, originalText) {
161
- if (keywordCase === 'upper') return keyword.toUpperCase();
162
- if (keywordCase === 'lower') return keyword.toLowerCase();
223
+ if (keywordCase === 'upper') {
224
+ return keyword.toUpperCase();
225
+ }
226
+ if (keywordCase === 'lower') {
227
+ return keyword.toLowerCase();
228
+ }
163
229
  return originalText || keyword;
164
230
  }
165
231
 
166
- /** Apply function casing rule */
232
+ /**
233
+ * Apply function casing rule
234
+ *
235
+ * @param name
236
+ */
167
237
  function function_(name) {
168
- if (functionCase === 'preserve') return name;
169
- if (functionCase === 'upper') return name.toUpperCase();
170
- if (functionCase === 'lower') return name.toLowerCase();
238
+ if (functionCase === 'preserve') {
239
+ return name;
240
+ }
241
+ if (functionCase === 'upper') {
242
+ return name.toUpperCase();
243
+ }
244
+ if (functionCase === 'lower') {
245
+ return name.toLowerCase();
246
+ }
171
247
  const canonical = FUNCTION_CANONICAL_MAP.get(name.toLowerCase()) || name;
172
248
  if (functionCase === 'lower-camel') {
173
249
  return canonical[0].toLowerCase() + canonical.slice(1);