prettier-plugin-sfmc 0.1.4 → 0.3.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,44 @@ 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, JavaScript / SSJS)
64
+
65
+ This plugin exports [Prettier `defaultOptions`](https://prettier.io/docs/plugins#defaultoptions). Prettier merges them from whichever plugin **owns the active printer** for the file being formatted. This package supplies printers for **AMPscript**, **SQL** (via composed `prettier-plugin-sql`), and the shared **`estree`** printer (the same implementation Prettier ships for JavaScript). User plugins are loaded **after** built-ins, so this plugin becomes the effective `estree` printer—meaning **`.ssjs`** files (typically `parser: "babel"`) pick up the table below **without** copying these keys into `.prettierrc`.
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` | Common JS style; aligns with `ampscriptQuoteStyle: 'single'` where the core quote option applies. |
73
+ | `trailingComma` | `'none'` | Avoids trailing commas that can break SSJS in some SFMC contexts. |
74
+
75
+ String delimiters inside AMPscript blocks still follow `ampscriptQuoteStyle`. See [Prettier options](https://prettier.io/docs/options) for every standard flag.
76
+
77
+ **Overrides:** Add options to `.prettierrc` or [overrides](https://prettier.io/docs/configuration#configuration-overrides) only when you want to **diverge** (for example `tabWidth: 2` for the whole project, or different rules per file glob).
78
+
79
+ **Scope:** With this plugin enabled, any file Prettier formats using the **`estree`** printer uses these defaults—not only `.ssjs`. If another plugin in your config also registers `printers.estree`, plugin **order** matters (the last one wins).
80
+
81
+ ## SQL (Transact-SQL / SFMC)
82
+
83
+ `.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.
84
+
85
+ **Defaults for SFMC-style T-SQL** (from composed [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql)):
86
+
87
+ | Option | Default | Other values | Rationale |
88
+ | ------ | ------- | ------------ | --------- |
89
+ | `language` | `tsql` | n/a | Must stay `tsql` for SFMC T-SQL. Other dialects are not supported for SFMC SQL; changing this can break formatting or behaviour. |
90
+ | `formatter` | `sql-formatter` | n/a | Must stay `sql-formatter` for SFMC SQL. Other formatters are not supported in this context; changing this can break. |
91
+ | `keywordCase` | `upper` | `preserve`, `lower` | Casing for reserved keywords. |
92
+ | `functionCase` | `upper` | `preserve`, `lower` | Casing for function names. |
93
+ | `identifierCase` | `preserve` | `upper`, `lower` | Unquoted identifiers only (upstream treats this as experimental). |
94
+ | `dataTypeCase` | `preserve` | `upper`, `lower` | Casing for data type names. |
95
+
96
+ Do **not** override `language` or `formatter` for SFMC. You may override the **casing** options in `.prettierrc` or under `overrides` with `files: "*.sql"` if you want different keyword/function/identifier/data-type casing.
97
+
98
+ 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
99
 
62
100
  ## HTML Embedding
63
101
 
@@ -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.4",
4
- "description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript formatting and SSJS file registration",
3
+ "version": "0.3.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 ./scripts/run-jest.mjs"
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",
@@ -40,12 +47,20 @@
40
47
  "dependencies": {
41
48
  "ampscript-data": "^0.1.3",
42
49
  "ampscript-parser": "^0.1.1",
50
+ "prettier-plugin-sql": "^0.20.0",
43
51
  "ssjs-data": "^0.2.2"
44
52
  },
45
53
  "devDependencies": {
54
+ "@eslint/js": "^10.0.1",
46
55
  "@jest/globals": "^29.0.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",
47
62
  "jest": "^29.0.0",
48
- "prettier": "^3.7.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,47 @@
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
+ * Re-exports Prettier’s `estree` printer so `defaultOptions` apply to JS/SSJS.
7
8
  *
8
9
  * Exports: languages, parsers, printers, options, defaultOptions
9
10
  * Following the Prettier v3 plugin API (ESM).
10
11
  */
11
12
 
12
13
  import { parse } from 'ampscript-parser';
14
+ import sqlPlugin from 'prettier-plugin-sql';
15
+ import { printers as prettierEstreePrinters } from 'prettier/plugins/estree';
13
16
  import { printAmpscriptNode, collectVariableMap } from './printer.js';
14
17
  import * as prettier from 'prettier';
15
18
 
19
+ /**
20
+ * Clone sql plugin option descriptors and set SFMC-friendly defaults.
21
+ *
22
+ * @param {Record<string, object>} sqlOptions
23
+ * @returns {Record<string, object>} Option map with adjusted defaults.
24
+ */
25
+ function mergeSqlOptionsWithSfmcDefaults(sqlOptions) {
26
+ /** @type {Record<string, object>} */
27
+ const merged = {};
28
+ for (const [key, descriptor] of Object.entries(sqlOptions)) {
29
+ merged[key] = { ...descriptor };
30
+ }
31
+ const defaults = {
32
+ language: 'tsql',
33
+ keywordCase: 'upper',
34
+ functionCase: 'upper',
35
+ identifierCase: 'preserve',
36
+ dataTypeCase: 'preserve',
37
+ };
38
+ for (const [key, defaultValue] of Object.entries(defaults)) {
39
+ if (merged[key]) {
40
+ merged[key].default = defaultValue;
41
+ }
42
+ }
43
+ return merged;
44
+ }
45
+
16
46
  // ── Languages ────────────────────────────────────────────────────────────────
17
47
 
18
48
  export const languages = [
@@ -28,6 +58,7 @@ export const languages = [
28
58
  extensions: ['.ssjs'],
29
59
  vscodeLanguageIds: ['ssjs'],
30
60
  },
61
+ ...sqlPlugin.languages,
31
62
  ];
32
63
 
33
64
  // ── Parsers ──────────────────────────────────────────────────────────────────
@@ -35,6 +66,7 @@ export const languages = [
35
66
  const PRAGMA_RE = /^\s*\/\*\*?\s*@(?:format|prettier)\s*\*\//;
36
67
 
37
68
  export const parsers = {
69
+ ...sqlPlugin.parsers,
38
70
  'ampscript-parse': {
39
71
  parse(text) {
40
72
  return parse(text);
@@ -52,7 +84,9 @@ export const parsers = {
52
84
  const blockClose = text.indexOf(']%%', blockOpen);
53
85
  if (blockClose !== -1) {
54
86
  const blockContent = text.slice(blockOpen + 3, blockClose);
55
- if (PRAGMA_RE.test(blockContent)) return true;
87
+ if (PRAGMA_RE.test(blockContent)) {
88
+ return true;
89
+ }
56
90
  }
57
91
  }
58
92
  return PRAGMA_RE.test(text);
@@ -63,6 +97,8 @@ export const parsers = {
63
97
  // ── Printers ─────────────────────────────────────────────────────────────────
64
98
 
65
99
  export const printers = {
100
+ ...sqlPlugin.printers,
101
+ estree: prettierEstreePrinters.estree,
66
102
  'ampscript-ast': {
67
103
  print(path, options, print) {
68
104
  const node = path.node;
@@ -76,12 +112,16 @@ export const printers = {
76
112
 
77
113
  embed(path, options) {
78
114
  const node = path.node;
79
- if (node.type !== 'Document') return;
115
+ if (node.type !== 'Document') {
116
+ return;
117
+ }
80
118
 
81
119
  const hasHtml = node.children.some(
82
120
  (c) => c.type === 'Content' && /<[a-zA-Z!/]/.test(c.value),
83
121
  );
84
- if (!hasHtml) return;
122
+ if (!hasHtml) {
123
+ return;
124
+ }
85
125
 
86
126
  return async (textToDocument, print) => {
87
127
  if (!options.__ampscriptVariableMap) {
@@ -111,19 +151,27 @@ export const printers = {
111
151
 
112
152
  const re = new RegExp(String.raw`${PH}(\d+)END`, 'g');
113
153
  return prettier.doc.utils.mapDoc(htmlDocument, (d) => {
114
- if (typeof d !== 'string') return d;
115
- if (!d.includes(PH)) return d;
154
+ if (typeof d !== 'string') {
155
+ return d;
156
+ }
157
+ if (!d.includes(PH)) {
158
+ return d;
159
+ }
116
160
 
117
161
  const parts = [];
118
162
  let last = 0;
119
163
  let m;
120
164
  re.lastIndex = 0;
121
165
  while ((m = re.exec(d)) !== null) {
122
- if (m.index > last) parts.push(d.slice(last, m.index));
166
+ if (m.index > last) {
167
+ parts.push(d.slice(last, m.index));
168
+ }
123
169
  parts.push(print(['children', Number.parseInt(m[1], 10)]));
124
170
  last = m.index + m[0].length;
125
171
  }
126
- if (last < d.length) parts.push(d.slice(last));
172
+ if (last < d.length) {
173
+ parts.push(d.slice(last));
174
+ }
127
175
  return parts.length === 1 ? parts[0] : parts;
128
176
  });
129
177
  };
@@ -143,7 +191,9 @@ export const printers = {
143
191
 
144
192
  printComment(path) {
145
193
  const comment = path.node;
146
- if (comment.type === 'Comment') return comment.value;
194
+ if (comment.type === 'Comment') {
195
+ return comment.value;
196
+ }
147
197
  return '';
148
198
  },
149
199
 
@@ -205,7 +255,7 @@ export const printers = {
205
255
 
206
256
  // ── Options ──────────────────────────────────────────────────────────────────
207
257
 
208
- export const options = {
258
+ const sfmcOptions = {
209
259
  ampscriptSpacing: {
210
260
  type: 'boolean',
211
261
  category: 'AMPscript',
@@ -292,12 +342,12 @@ export const options = {
292
342
  ampscriptVarDeclarationStyle: {
293
343
  type: 'choice',
294
344
  category: 'AMPscript',
295
- default: 'auto',
345
+ default: 'multi-line',
296
346
  choices: [
297
347
  {
298
348
  value: 'auto',
299
349
  description:
300
- "Let Prettier decide based on printWidth: single line when it fits, one-per-line when it doesn't.",
350
+ 'Let Prettier decide based on printWidth: single line when it fits, otherwise break after each comma.',
301
351
  },
302
352
  {
303
353
  value: 'single-line',
@@ -305,16 +355,25 @@ export const options = {
305
355
  },
306
356
  {
307
357
  value: 'multi-line',
308
- description: 'Always place each variable on its own line.',
358
+ description:
359
+ 'Always break after each comma: one `var` keyword, variables listed on separate lines.',
309
360
  },
310
361
  ],
311
362
  description: 'Control how var declarations with multiple variables are formatted.',
312
363
  },
313
364
  };
314
365
 
366
+ export const options = {
367
+ ...mergeSqlOptionsWithSfmcDefaults(sqlPlugin.options),
368
+ ...sfmcOptions,
369
+ };
370
+
315
371
  // ── Default Options ──────────────────────────────────────────────────────────
316
372
 
317
373
  export const defaultOptions = {
318
- tabWidth: 2,
319
374
  useTabs: false,
375
+ tabWidth: 4,
376
+ printWidth: 100,
377
+ singleQuote: true,
378
+ trailingComma: 'none',
320
379
  };
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);