prettier-plugin-sfmc 0.2.0 → 0.4.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
@@ -36,7 +36,7 @@ All options use the `ampscript` prefix — they control AMPscript formatting beh
36
36
  | [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md) | `"single"` \| `"double"` | `"single"` | String quote style |
37
37
  | [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md) | `"lower"` \| `"upper"` \| `"preserve"` | `"lower"` | Keyword casing |
38
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 |
39
+ | [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md) | boolean | `false` | Optional line breaks around `%%[ ]%%` when not already at a line boundary |
40
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`
@@ -60,41 +60,40 @@ All options use the `ampscript` prefix — they control AMPscript formatting beh
60
60
  | `.ssjs` | `babel` (built-in) | Standard JavaScript formatting |
61
61
  | `.sql` | `sql` | SQL via composed `prettier-plugin-sql` |
62
62
 
63
- ## Core Prettier defaults (AMPscript, HTML, SQL)
63
+ ## Core Prettier defaults (AMPscript, HTML, SQL, JavaScript / SSJS)
64
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.
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
66
 
67
67
  | Option | Default | Rationale |
68
68
  | ------ | ------- | --------- |
69
69
  | `useTabs` | `false` | SFMC often normalizes tabs away on save; spaces keep layout stable. |
70
70
  | `tabWidth` | `4` | Readable indentation (override in config if you prefer 2). |
71
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). |
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
74
 
75
75
  String delimiters inside AMPscript blocks still follow `ampscriptQuoteStyle`. See [Prettier options](https://prettier.io/docs/options) for every standard flag.
76
76
 
77
- ### SSJS (`.ssjs`)
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
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.
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).
92
80
 
93
81
  ## SQL (Transact-SQL / SFMC)
94
82
 
95
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.
96
84
 
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.
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.
98
97
 
99
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.
100
99
 
@@ -1,25 +1,31 @@
1
1
  # `ampscriptBlockLineBreaks`
2
2
 
3
- > Enforce blank lines before and after `%%[ … ]%%` block expressions.
3
+ > Optionally insert line breaks before and after `%%[ … ]%%` block expressions when they are **not** already at a line boundary in the source.
4
4
 
5
5
  | | |
6
6
  |---|---|
7
7
  | **Type** | `boolean` |
8
- | **Default** | `true` |
8
+ | **Default** | `false` |
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
- Adds an empty line before and after every AMPscript block (`%%[ ]%%`) to visually separate logic from surrounding HTML or text content. Inline expressions (`%%= =%%`) are never affected. Set to `false` when blocks are embedded in contexts such as attribute values or tightly formatted output sections — where extra blank lines would break the template layout.
13
+ When set to `true`, Prettier may add a **single** mandatory line break before the opening delimiter and/or after the closing delimiter so the block is not glued to other text on the same line (for example `</p>%%[` or `]%%<p>`).
14
+
15
+ Breaks are **not** added when the opening is already at the start of a logical line (beginning of file after optional UTF-8 BOM, optional spaces/tabs only before `%%[`, or immediately after `\n` / `\r\n` / `\r`), or when the closing is already at the end of a logical line (only horizontal whitespace until the next line break or end of file). That keeps formatting **idempotent** on repeated save instead of growing extra blank lines.
16
+
17
+ Inline expressions (`%%= … =%%`) are never affected.
18
+
19
+ The default is `false` so SMS and other line-break–sensitive templates are not altered unless you opt in.
14
20
 
15
21
  ## Settings
16
22
 
17
23
  | Value | Effect |
18
24
  |-------|--------|
19
- | `true` (default) | Blank line inserted before and after every `%%[ ]%%` block |
20
- | `false` | No blank lines added; blocks formatted flush with surrounding content |
25
+ | `false` (default) | No extra outer line breaks from this option; inner block layout still normalizes |
26
+ | `true` | Add outer line breaks only where the block is not already isolated on its own line |
21
27
 
22
- ### `true` (default)
28
+ ### `true` — inline-adjacent block
23
29
 
24
30
  **Input:**
25
31
 
@@ -30,35 +36,26 @@ set @name = "Alice"
30
36
  ]%%<p>%%=v(@name)=%%</p>
31
37
  ```
32
38
 
33
- **Output:**
34
-
35
- ```html
36
- <p>Hello</p>
39
+ **Output** (illustrative; exact HTML wrapping may include normal Prettier line breaks):
37
40
 
38
- %%[
39
- var @name
40
- set @name = "Alice"
41
- ]%%
41
+ - A line break is inserted so `%%[` is not on the same line as `</p>`.
42
+ - A line break is inserted so `]%%` is not on the same line as `<p>`.
42
43
 
43
- <p>%%= v(@name) =%%</p>
44
- ```
44
+ ### `true` — already line-isolated (no extra outer breaks)
45
45
 
46
- ### `false`
46
+ If the source already has `%%[` at the beginning of a line (after optional BOM / spaces / tabs) and `]%%` at the end of a line (before optional spaces and `\n` or EOF), enabling this option does **not** add further outer breaks, so the document does not grow on every format.
47
47
 
48
- **Output:**
48
+ ### `false` (default)
49
49
 
50
- ```html
51
- <p>Hello</p>%%[
52
- var @name
53
- set @name = "Alice"
54
- ]%%<p>%%= v(@name) =%%</p>
55
- ```
50
+ **Output:** blocks stay flush with surrounding content except for whatever the HTML / AMPscript printers do for normal indentation and line wrapping.
56
51
 
57
52
  ## Configuration Example
58
53
 
54
+ Opt in when you want blocks separated from inline HTML only where needed:
55
+
59
56
  ```json
60
57
  {
61
58
  "plugins": ["prettier-plugin-sfmc"],
62
- "ampscriptBlockLineBreaks": false
59
+ "ampscriptBlockLineBreaks": true
63
60
  }
64
61
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-sfmc",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript, SSJS, and SQL formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,7 @@
27
27
  "test": "node ./scripts/run-jest.mjs",
28
28
  "lint": "eslint .",
29
29
  "lint:fix": "eslint --fix .",
30
+ "prepare": "husky || true",
30
31
  "format": "prettier --write .",
31
32
  "format:check": "prettier --check ."
32
33
  },
@@ -59,6 +60,7 @@
59
60
  "eslint-plugin-prettier": "^5.5.0",
60
61
  "eslint-plugin-unicorn": "^64.0.0",
61
62
  "globals": "^17.4.0",
63
+ "husky": "^9.1.7",
62
64
  "jest": "^29.0.0",
63
65
  "prettier": "^3.8.1"
64
66
  },
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Detect whether AMPscript block delimiters already sit on their own logical
3
+ * lines in the original source (LF, CRLF, or CR). Used to avoid Prettier
4
+ * hardlines that would accumulate on repeated format.
5
+ *
6
+ * @param {string} ch
7
+ */
8
+ function isHorizontalSpaceOrBom(ch) {
9
+ return ch === ' ' || ch === '\t' || ch === '\uFEFF';
10
+ }
11
+
12
+ /**
13
+ * True if `index` is the first non–BOM/horizontal-ws column of a line
14
+ * (BOF, or only BOM/ws before index, or newline before index after skipping ws).
15
+ *
16
+ * @param {string} text
17
+ * @param {number} index
18
+ */
19
+ function isIndexAtLineStart(text, index) {
20
+ if (index <= 0) {
21
+ return true;
22
+ }
23
+
24
+ let i = index - 1;
25
+ while (i >= 0 && isHorizontalSpaceOrBom(text[i])) {
26
+ i--;
27
+ }
28
+
29
+ if (i < 0) {
30
+ return true;
31
+ }
32
+
33
+ const ch = text[i];
34
+ return ch === '\n' || ch === '\r';
35
+ }
36
+
37
+ /**
38
+ * True if everything from `index` to EOF is horizontal ws/BOM, or the first
39
+ * non-ws character begins a new line.
40
+ *
41
+ * @param {string} text
42
+ * @param {number} index
43
+ */
44
+ function isIndexAtLineEnd(text, index) {
45
+ const len = text.length;
46
+ let j = index;
47
+
48
+ while (j < len && isHorizontalSpaceOrBom(text[j])) {
49
+ j++;
50
+ }
51
+
52
+ if (j >= len) {
53
+ return true;
54
+ }
55
+
56
+ const ch = text[j];
57
+ return ch === '\n' || ch === '\r';
58
+ }
59
+
60
+ /**
61
+ * When ampscriptBlockLineBreaks is enabled, add a leading hardline only if
62
+ * the block opening is not already at the start of a line in the source.
63
+ *
64
+ * @param {string | undefined} originalText
65
+ * @param {number | undefined} blockStart
66
+ */
67
+ export function needsLeadingBlockBreak(originalText, blockStart) {
68
+ if (typeof originalText !== 'string' || blockStart == null || blockStart < 0) {
69
+ return false;
70
+ }
71
+
72
+ return !isIndexAtLineStart(originalText, blockStart);
73
+ }
74
+
75
+ /**
76
+ * When ampscriptBlockLineBreaks is enabled, add a trailing hardline only if
77
+ * the block closing is not already at the end of a line in the source.
78
+ *
79
+ * @param {string | undefined} originalText
80
+ * @param {number | undefined} blockEnd
81
+ */
82
+ export function needsTrailingBlockBreak(originalText, blockEnd) {
83
+ if (typeof originalText !== 'string' || blockEnd == null || blockEnd < 0) {
84
+ return false;
85
+ }
86
+
87
+ return !isIndexAtLineEnd(originalText, blockEnd);
88
+ }
package/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * A unified Prettier plugin for Salesforce Marketing Cloud.
5
5
  * Handles AMPscript formatting (.ampscript, .amp, .html), SSJS file
6
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).
@@ -11,6 +12,7 @@
11
12
 
12
13
  import { parse } from 'ampscript-parser';
13
14
  import sqlPlugin from 'prettier-plugin-sql';
15
+ import { printers as prettierEstreePrinters } from 'prettier/plugins/estree';
14
16
  import { printAmpscriptNode, collectVariableMap } from './printer.js';
15
17
  import * as prettier from 'prettier';
16
18
 
@@ -96,6 +98,7 @@ export const parsers = {
96
98
 
97
99
  export const printers = {
98
100
  ...sqlPlugin.printers,
101
+ estree: prettierEstreePrinters.estree,
99
102
  'ampscript-ast': {
100
103
  print(path, options, print) {
101
104
  const node = path.node;
@@ -330,11 +333,12 @@ const sfmcOptions = {
330
333
  ampscriptBlockLineBreaks: {
331
334
  type: 'boolean',
332
335
  category: 'AMPscript',
333
- default: true,
336
+ default: false,
334
337
  description:
335
- 'Enforce line breaks before and after %%[ ... ]%% blocks. ' +
338
+ 'When true, insert line breaks before and after %%[ ... ]%% blocks only when the ' +
339
+ 'opening/closing delimiters are not already at a line boundary in the source. ' +
336
340
  'Inline expressions (%%= ... =%%) are never affected. ' +
337
- 'Set to false when blocks are embedded in contexts where extra line breaks are unwanted.',
341
+ 'Default is false to avoid surprising line breaks in SMS and similar contexts.',
338
342
  },
339
343
  ampscriptVarDeclarationStyle: {
340
344
  type: 'choice',
package/src/printer.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import * as prettier from 'prettier';
10
10
  import { FUNCTION_CANONICAL_MAP, AMPSCRIPT_KEYWORDS } from 'ampscript-data';
11
+ import { needsLeadingBlockBreak, needsTrailingBlockBreak } from './ampscript-block-boundaries.js';
11
12
 
12
13
  const { group, indent, join, line, softline, hardline } = prettier.doc.builders;
13
14
 
@@ -203,7 +204,7 @@ function printAmpscriptNode(path, options, print) {
203
204
  const variableStyle = options.ampscriptVarDeclarationStyle || 'multi-line';
204
205
  const keywordCase = options.ampscriptKeywordCase || 'lower';
205
206
  const functionCase = options.ampscriptFunctionCase || 'upper-camel';
206
- const blockLineBreaks = options.ampscriptBlockLineBreaks !== false;
207
+ const blockLineBreaks = options.ampscriptBlockLineBreaks === true;
207
208
 
208
209
  function resolveVariable(variableName) {
209
210
  if (!enforceCasing) {
@@ -295,8 +296,21 @@ function printAmpscriptNode(path, options, print) {
295
296
  blockClose,
296
297
  ]);
297
298
  if (blockLineBreaks) {
298
- return [hardline, blockDocument, hardline];
299
+ const originalText = options.originalText;
300
+ const parts = [];
301
+ if (needsLeadingBlockBreak(originalText, node.start)) {
302
+ parts.push(hardline);
303
+ }
304
+
305
+ parts.push(blockDocument);
306
+
307
+ if (needsTrailingBlockBreak(originalText, node.end)) {
308
+ parts.push(hardline);
309
+ }
310
+
311
+ return parts.length === 1 ? parts[0] : parts;
299
312
  }
313
+
300
314
  return blockDocument;
301
315
  }
302
316