sf-decomposer 6.13.0 → 6.15.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/CHANGELOG.md +20 -0
- package/HANDBOOK.md +286 -0
- package/README.md +151 -16
- package/lib/commands/decomposer/verify.d.ts +17 -0
- package/lib/commands/decomposer/verify.js +83 -0
- package/lib/commands/decomposer/verify.js.map +1 -0
- package/lib/core/verifyMetadataTypes.d.ts +12 -0
- package/lib/core/verifyMetadataTypes.js +143 -0
- package/lib/core/verifyMetadataTypes.js.map +1 -0
- package/lib/helpers/configOverrides.d.ts +28 -0
- package/lib/helpers/configOverrides.js +125 -0
- package/lib/helpers/configOverrides.js.map +1 -1
- package/lib/helpers/types.d.ts +44 -0
- package/lib/service/decompose/decomposeFileHandler.js +22 -9
- package/lib/service/decompose/decomposeFileHandler.js.map +1 -1
- package/lib/service/verify/diffDirectories.d.ts +34 -0
- package/lib/service/verify/diffDirectories.js +134 -0
- package/lib/service/verify/diffDirectories.js.map +1 -0
- package/messages/decomposer.verify.md +57 -0
- package/oclif.manifest.json +122 -1
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
7
7
|
|
|
8
|
+
## [6.15.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.14.0...v6.15.0) (2026-04-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* support multi-rule multiLevel overrides ([#419](https://github.com/mcarvin8/sf-decomposer/issues/419)) ([5b36f74](https://github.com/mcarvin8/sf-decomposer/commit/5b36f745ab998509b2b7a86a1a6d0a65ebc91a45))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **bot:** round-trip bots with nested botSteps multi-level rule ([#421](https://github.com/mcarvin8/sf-decomposer/issues/421)) ([38e8f9e](https://github.com/mcarvin8/sf-decomposer/commit/38e8f9e944b6ba85d724c479c28f9f3de1e9361a))
|
|
19
|
+
|
|
20
|
+
## [6.14.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.13.0...v6.14.0) (2026-04-30)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* add multiLevel override and round-trip verify command ([#418](https://github.com/mcarvin8/sf-decomposer/issues/418)) ([5f06265](https://github.com/mcarvin8/sf-decomposer/commit/5f06265333e9a0523154d0c745e83e0571938f98))
|
|
26
|
+
* **decompose:** expose splitTags as an override field ([#416](https://github.com/mcarvin8/sf-decomposer/issues/416)) ([eedf0f2](https://github.com/mcarvin8/sf-decomposer/commit/eedf0f23d248fdd028a097435aa9493449269954))
|
|
27
|
+
|
|
8
28
|
## [6.13.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.12.0...v6.13.0) (2026-04-30)
|
|
9
29
|
|
|
10
30
|
|
package/HANDBOOK.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Admin Handbook — Decomposing Tricky Salesforce Metadata
|
|
2
|
+
|
|
3
|
+
This handbook collects ready-to-paste `.sfdecomposer.config.json` recipes for the metadata types where Salesforce's native source format either doesn't decompose at all or decomposes too coarsely to diff well in version control. Every recipe in this guide:
|
|
4
|
+
|
|
5
|
+
- decomposes the original file into per-piece units that survive `git diff` cleanly,
|
|
6
|
+
- recomposes back to a **byte-identical** XML deployable to any org,
|
|
7
|
+
- is verified by `sf decomposer verify` before you commit it.
|
|
8
|
+
|
|
9
|
+
If you want the underlying option grammar instead of recipes, see the [main README](./README.md#per-type--per-component-overrides).
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Choosing a strategy](#choosing-a-strategy)
|
|
14
|
+
- [Bots (Agentforce and Einstein)](#bots-agentforce-and-einstein)
|
|
15
|
+
- [Flexipages (Lightning App / Record / Home pages)](#flexipages-lightning-app--record--home-pages)
|
|
16
|
+
- [Layouts (page layouts)](#layouts-page-layouts)
|
|
17
|
+
- [Other deeply-nested types](#other-deeply-nested-types)
|
|
18
|
+
- [The verification workflow](#the-verification-workflow)
|
|
19
|
+
- [Common pitfalls](#common-pitfalls)
|
|
20
|
+
|
|
21
|
+
## Choosing a strategy
|
|
22
|
+
|
|
23
|
+
Three knobs cover almost every case:
|
|
24
|
+
|
|
25
|
+
| Symptom of the source XML | Reach for |
|
|
26
|
+
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| One repeating section at the top level (e.g. `<labels>` of a custom-labels file). | `strategy: unique-id` (default). |
|
|
28
|
+
| Lots of small repeatable tags, you want one file per tag-name, not per-instance. | `strategy: grouped-by-tag`. |
|
|
29
|
+
| `grouped-by-tag`, but a few specific tags (e.g. `objectPermissions`) need finer-grained diff. | Add `splitTags`. |
|
|
30
|
+
| A deeply-nested repeatable block lives _inside_ another repeatable block (the bot pattern). | Add `multiLevel`. If there are several such nested blocks, pass them as an array — see Bots below. |
|
|
31
|
+
|
|
32
|
+
Hard rules the plugin always enforces (so you don't have to):
|
|
33
|
+
|
|
34
|
+
- `labels` and `loyaltyProgramSetup` are always treated as `unique-id` regardless of any override.
|
|
35
|
+
- `loyaltyProgramSetup` automatically gets `multiLevel: programProcesses:programProcesses:parameterName,ruleName` when run under `unique-id` — you can replace it but you don't have to set it.
|
|
36
|
+
|
|
37
|
+
Everything else in this handbook is opt-in.
|
|
38
|
+
|
|
39
|
+
## Bots (Agentforce and Einstein)
|
|
40
|
+
|
|
41
|
+
**Why this is hard.** Agentforce and Einstein bots both ship as `.bot-meta.xml`, but their internals diverge:
|
|
42
|
+
|
|
43
|
+
- The bot **header** (`Bot.bot-meta.xml`) is small — a few leaves, a few `botUserList` entries.
|
|
44
|
+
- The bot **versions** (`vN.botVersion-meta.xml`) are the painful ones. A single `BotVersion` typically contains:
|
|
45
|
+
- `botDialogGroups` (logical groupings),
|
|
46
|
+
- `botDialogs` (the dialogs themselves), and inside each dialog,
|
|
47
|
+
- `botSteps` (often nested 2–3 levels deep — message, collect, transfer, condition, ...),
|
|
48
|
+
- `mlIntents`, `conversationVariables`, `botStepConditions`, ...
|
|
49
|
+
|
|
50
|
+
A flat `unique-id` decomposition of a `BotVersion` produces one giant per-dialog file with hundreds of nested steps inside. That's only marginally easier to review than the original.
|
|
51
|
+
|
|
52
|
+
**Recipe — two-rule multi-level decomposition.**
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"metadataSuffixes": "bot",
|
|
57
|
+
"strategy": "unique-id",
|
|
58
|
+
"decomposedFormat": "xml",
|
|
59
|
+
"overrides": [
|
|
60
|
+
{
|
|
61
|
+
"metadataTypes": ["bot"],
|
|
62
|
+
"multiLevel": ["botDialogs:botDialogs:developerName", "botSteps:botSteps:type"]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
What this produces on disk for `Sample_Chat_Bot/v1.botVersion-meta.xml` (an Einstein chat bot from the plugin's own fixtures):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
bots/
|
|
72
|
+
└── Sample_Chat_Bot/
|
|
73
|
+
├── Sample_Chat_Bot.bot-meta.xml ← bot header (untouched)
|
|
74
|
+
├── v1/
|
|
75
|
+
│ ├── nlpProviders/
|
|
76
|
+
│ │ └── EinsteinAi.nlpProviders-meta.xml
|
|
77
|
+
│ ├── botDialogs/ ← outer rule lands here
|
|
78
|
+
│ │ ├── Welcome/ ← one directory per dialog (named by developerName)
|
|
79
|
+
│ │ │ ├── Welcome.xml ← dialog leaf properties
|
|
80
|
+
│ │ │ └── botSteps/ ← inner rule lands here
|
|
81
|
+
│ │ │ ├── 853b6432/ ← step with nested content -> own subdir
|
|
82
|
+
│ │ │ │ ├── 853b6432.xml ← step leaf properties
|
|
83
|
+
│ │ │ │ └── botNavigation/ ← nested step content broken out
|
|
84
|
+
│ │ │ │ └── Redirect.botNavigation-meta.xml
|
|
85
|
+
│ │ │ └── dc35b789/
|
|
86
|
+
│ │ │ ├── dc35b789.xml
|
|
87
|
+
│ │ │ └── botMessages/
|
|
88
|
+
│ │ │ └── dc35b789.botMessages-meta.xml
|
|
89
|
+
│ │ ├── End_Chat/
|
|
90
|
+
│ │ │ ├── End_Chat.xml
|
|
91
|
+
│ │ │ └── botSteps/
|
|
92
|
+
│ │ │ ├── 9d031e75.botSteps-meta.xml ← step with no nested content -> single leaf file
|
|
93
|
+
│ │ │ └── a7afda99/ ← step with nested content -> subdir
|
|
94
|
+
│ │ │ └── ...
|
|
95
|
+
│ │ └── ...
|
|
96
|
+
│ ├── .multi_level.json ← required for recompose; do not hand-edit
|
|
97
|
+
│ └── v1.botVersion-meta.xml ← leaf-only outer wrapper
|
|
98
|
+
└── ...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A few things worth knowing before you commit this:
|
|
102
|
+
|
|
103
|
+
- **Dialog folders are named by developerName**, the way the recipe asks for. So `Welcome`, `End_Chat`, etc. are stable and review-friendly across deploys.
|
|
104
|
+
- **Step folders/files are content-hashed.** The inner rule's `:type` segment tells the disassembler what to look for, but each step's nested content is what determines the on-disk name (`853b6432`, `dc35b789`, ...). The hashes are stable across runs as long as the step content doesn't change, so they diff cleanly. Don't try to "rename them to something nicer" — they'll regenerate on the next decompose.
|
|
105
|
+
- **Each step is one of two shapes**: a leaf `<hash>.botSteps-meta.xml` file when the step has no nested content (e.g. a `Wait` step), or a `<hash>/` directory containing `<hash>.xml` plus subdirectories for the nested content (e.g. a `Message` step with `<botMessages>`, a `Navigation` step with `<botNavigation>`, an Agentforce `Action` step with `<botFlowInvocation>`). Both shapes recompose back to identical `<botSteps>` XML.
|
|
106
|
+
- **The two rules do different things.** The outer `botDialogs` rule is what gives you per-dialog folders — that's the headline win for review-ability. The inner `botSteps` rule additionally splits each step's nested content out of the per-dialog file into per-step subdirectories. For small Einstein bots with shallow steps you can drop the inner rule and still get the dialog-level split; for heavier Agentforce bots the inner rule is the difference between a 50-line per-step subtree and a 500-line per-dialog file.
|
|
107
|
+
|
|
108
|
+
**When to use a single rule instead.** If your bots have shallow steps (most Einstein chat bots, or pre-Agentforce bots), `multiLevel: ["botDialogs:botDialogs:developerName"]` alone is enough. Each dialog gets its own folder and steps live as flat `*.botSteps-meta.xml` files inside. Recompose is byte-identical either way; the choice is purely about how granular you want the per-step diff to be.
|
|
109
|
+
|
|
110
|
+
> **Per-bot precision.** Scope the override to a single component when one bot in your repo has a different shape than the rest. The plugin's own bot fixture suite uses this pattern:
|
|
111
|
+
>
|
|
112
|
+
> ```json
|
|
113
|
+
> {
|
|
114
|
+
> "components": ["bot:Sample_Chat_Bot", "bot:Internal_Copilot_Sample"],
|
|
115
|
+
> "multiLevel": ["botDialogs:botDialogs:developerName", "botSteps:botSteps:type"]
|
|
116
|
+
> }
|
|
117
|
+
> ```
|
|
118
|
+
|
|
119
|
+
> **Agentforce vs Einstein.** Both share the `bot` suffix, so a single override entry covers both bot families. The structural difference (Agentforce uses richer `botFlowInvocation` and `genAi*` blocks where Einstein uses `botNavigation` and `mlIntents`) is invisible to this recipe — `multiLevel` rules only target the repeating sections that exist on each side. The plugin ships an `Internal_Copilot_Sample` fixture that exercises the Agentforce-style shape and an Einstein-style `Sample_Chat_Bot` fixture; both round-trip with the same recipe.
|
|
120
|
+
|
|
121
|
+
> **Sibling order inside `botSteps`.** Recompose orders `<botSteps>` siblings alphabetically by their on-disk filename, not by their original document position. For bot deploys this is a no-op (Salesforce doesn't rely on step order at the XML level), but a freshly-recomposed file may show step entries shuffled compared to the source you originally pulled. The committed fixtures in this repo are baked from the recompose output for that reason — `sf decomposer verify` will treat the baked output as the source of truth.
|
|
122
|
+
|
|
123
|
+
## Flexipages (Lightning App / Record / Home pages)
|
|
124
|
+
|
|
125
|
+
**Why this is hard.** A non-trivial flexipage looks like:
|
|
126
|
+
|
|
127
|
+
```xml
|
|
128
|
+
<FlexiPage>
|
|
129
|
+
<flexiPageRegions>
|
|
130
|
+
<itemInstances>
|
|
131
|
+
<componentInstance>
|
|
132
|
+
<componentInstanceProperties>...</componentInstanceProperties>
|
|
133
|
+
<componentName>...</componentName>
|
|
134
|
+
</componentInstance>
|
|
135
|
+
</itemInstances>
|
|
136
|
+
<itemInstances>...</itemInstances>
|
|
137
|
+
...
|
|
138
|
+
</flexiPageRegions>
|
|
139
|
+
<flexiPageRegions>...</flexiPageRegions>
|
|
140
|
+
</FlexiPage>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Two repeating layers (`flexiPageRegions → itemInstances`) and component identity is buried inside `componentInstance` — the surrounding wrappers don't have a stable name, so a naive `unique-id` pass produces hash-named files that churn whenever a component is reordered.
|
|
144
|
+
|
|
145
|
+
**Recipe — flexiPageRegions out, then itemInstances per region.**
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"overrides": [
|
|
150
|
+
{
|
|
151
|
+
"metadataTypes": ["flexipage"],
|
|
152
|
+
"strategy": "unique-id",
|
|
153
|
+
"multiLevel": ["flexiPageRegions:flexiPageRegions:name", "itemInstances:itemInstances:componentName,facetId"]
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
What you'll see on disk:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
flexipages/
|
|
163
|
+
└── Account_Record_Page/
|
|
164
|
+
├── flexiPageRegions/
|
|
165
|
+
│ ├── header.flexiPageRegions-meta.xml
|
|
166
|
+
│ ├── main.flexiPageRegions-meta.xml
|
|
167
|
+
│ └── main/
|
|
168
|
+
│ └── itemInstances/
|
|
169
|
+
│ ├── force_highlightsPanel.itemInstances-meta.xml
|
|
170
|
+
│ ├── runtime_sales_activities__activitiesComponent.itemInstances-meta.xml
|
|
171
|
+
│ └── ...
|
|
172
|
+
└── Account_Record_Page.flexipage-meta.xml
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**When to adjust.**
|
|
176
|
+
|
|
177
|
+
- Flexipages where regions are addressed by `regionId` instead of `name` — swap the first rule to `flexiPageRegions:flexiPageRegions:regionId,name`.
|
|
178
|
+
- If the same `componentName` appears multiple times in one region (common for blank `force:emptySpace`), include `facetId` (already in the recipe) and, if needed, a stable property: `itemInstances:itemInstances:componentName,facetId,componentInstanceProperties.value`.
|
|
179
|
+
|
|
180
|
+
## Layouts (page layouts)
|
|
181
|
+
|
|
182
|
+
**Why this is hard.** Layouts have three nested repeatables:
|
|
183
|
+
|
|
184
|
+
```xml
|
|
185
|
+
<Layout>
|
|
186
|
+
<layoutSections>
|
|
187
|
+
<layoutColumns>
|
|
188
|
+
<layoutItems>
|
|
189
|
+
<field>...</field>
|
|
190
|
+
</layoutItems>
|
|
191
|
+
</layoutColumns>
|
|
192
|
+
</layoutSections>
|
|
193
|
+
</Layout>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For a fat object (Account, Opportunity), this often runs to thousands of lines. Reviewing a "moved one field from column 1 to column 2" change in the raw XML is painful.
|
|
197
|
+
|
|
198
|
+
**Recipe — section out, item per field.**
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"overrides": [
|
|
203
|
+
{
|
|
204
|
+
"metadataTypes": ["layout"],
|
|
205
|
+
"strategy": "unique-id",
|
|
206
|
+
"multiLevel": ["layoutSections:layoutSections:label", "layoutItems:layoutItems:field,customLink,emptySpace"]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
What you'll see on disk:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
layouts/
|
|
216
|
+
└── Account-Account_Layout/
|
|
217
|
+
├── layoutSections/
|
|
218
|
+
│ ├── Account_Information.layoutSections-meta.xml
|
|
219
|
+
│ ├── Address_Information.layoutSections-meta.xml
|
|
220
|
+
│ └── Account_Information/
|
|
221
|
+
│ └── layoutItems/
|
|
222
|
+
│ ├── Name.layoutItems-meta.xml
|
|
223
|
+
│ ├── Type.layoutItems-meta.xml
|
|
224
|
+
│ └── ...
|
|
225
|
+
└── Account-Account_Layout.layout-meta.xml
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Caveats.**
|
|
229
|
+
|
|
230
|
+
- Empty-space layout items (`<emptySpace>true</emptySpace>` with no `field`) all collapse to the same key. The recipe above falls through to `customLink` and then `emptySpace`, but if you have many empty-space spacers per section you'll get hash-named tiebreakers. That's fine for diffs (they only churn when the layout changes), just be aware.
|
|
231
|
+
- Sections with duplicate labels are unusual but legal (e.g. two "Custom Links" sections). If you hit collisions, add a stable secondary like `style`: `layoutSections:layoutSections:label,style`.
|
|
232
|
+
|
|
233
|
+
## Other deeply-nested types
|
|
234
|
+
|
|
235
|
+
These follow the same pattern; pick the rules that match your repo's data. None of these are decomposed natively by Salesforce.
|
|
236
|
+
|
|
237
|
+
| Metadata type | Suggested override |
|
|
238
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `flow` | `multiLevel: ["actionCalls:actionCalls:name", "decisions:decisions:name", "rules:rules:name"]` |
|
|
240
|
+
| `globalValueSet` | `multiLevel: ["customValue:customValue:fullName"]` — handy when value sets have hundreds of picks. |
|
|
241
|
+
| `marketingappextension` | `multiLevel: ["activityDefinitions:activityDefinitions:apiName"]` |
|
|
242
|
+
| `cmsDeliveryChannel` (and other CMS types) | `strategy: grouped-by-tag` plus `splitTags` for any wide repeatable tag. |
|
|
243
|
+
| `dashboard` | `multiLevel: ["components:components:title"]` — one file per dashboard widget. |
|
|
244
|
+
|
|
245
|
+
If a metadata type has a single deeply-nested repeatable block, a one-rule `multiLevel` is enough. Reach for the array form only when you have **two or more** distinct nested sections you want addressable on disk.
|
|
246
|
+
|
|
247
|
+
## The verification workflow
|
|
248
|
+
|
|
249
|
+
Always verify a new override before committing it:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# 1. Stash any uncommitted source first.
|
|
253
|
+
git stash --include-untracked
|
|
254
|
+
|
|
255
|
+
# 2. Decompose with the new override.
|
|
256
|
+
sf decomposer decompose -t bot --config
|
|
257
|
+
|
|
258
|
+
# 3. Recompose back from the decomposed tree.
|
|
259
|
+
sf decomposer recompose -t bot
|
|
260
|
+
|
|
261
|
+
# 4. Check the round-trip didn't drift.
|
|
262
|
+
sf decomposer verify -t bot --config
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`sf decomposer verify` is non-destructive: it decomposes into a temp dir, recomposes from the temp dir, and compares the result to your committed source. If anything drifts (content, missing file, sibling reorder) it tells you exactly which paths broke. Treat any drift as a blocker — fix the override (or fall back to the previous one) before committing.
|
|
266
|
+
|
|
267
|
+
## Common pitfalls
|
|
268
|
+
|
|
269
|
+
**1. "I added two `multiLevel` rules but only one survived."**
|
|
270
|
+
You probably ran two `decompose` invocations back-to-back, one rule each. Don't. The disassembler rewrites `.multi_level.json` on every run, so each call replaces the prior one. Pass every rule for a given component in **one** override entry, in array form.
|
|
271
|
+
|
|
272
|
+
**2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
|
|
273
|
+
Almost always a unique-id collision. Two array items resolved to the same filename and one overwrote the other on disk. Add a tiebreaker to `unique_id_elements` (e.g. `developerName,id`) and re-decompose with `prePurge: true`.
|
|
274
|
+
|
|
275
|
+
**3. "Component-scope override fields look ignored."**
|
|
276
|
+
Component-scope wins over type-scope, but only for fields **the component override explicitly sets**. Fields it leaves out fall through to the type-scope value, then to the run-wide default. If you set `decomposedFormat: "yaml"` on a type and `strategy: "grouped-by-tag"` on the component, the component still gets `decomposedFormat: "yaml"` from the type override.
|
|
277
|
+
|
|
278
|
+
**4. "Reassembly removed my decomposed directory even though I didn't pass `postPurge`."**
|
|
279
|
+
That's by design for `multiLevel` types only. Multi-level recompose has to clean up inner-level directories so the next level can merge their reassembled XML. If you want the decomposed tree preserved for inspection, copy it before running `recompose`.
|
|
280
|
+
|
|
281
|
+
**5. "Decompose succeeded but my decomposed files all have hash names."**
|
|
282
|
+
Your `unique_id_elements` (or the rule's third part) didn't resolve to a non-empty value on those items. Check the source XML for the elements you listed — names are case-sensitive and live at the immediate child level of each repeating item. The plugin only walks one level deep when picking a UID.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
When in doubt: write the override, run `verify`, read the diff. Every recipe in this handbook was derived that way.
|
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
|
|
|
17
17
|
- [Commands](#commands)
|
|
18
18
|
- [sf decomposer decompose](#sf-decomposer-decompose)
|
|
19
19
|
- [sf decomposer recompose](#sf-decomposer-recompose)
|
|
20
|
-
- [
|
|
20
|
+
- [sf decomposer verify](#sf-decomposer-verify)
|
|
21
|
+
- [Manifest-scoped runs](#manifest-scoped-runs)
|
|
21
22
|
- [Decompose Strategies](#decompose-strategies)
|
|
22
23
|
- [Custom Labels](#custom-labels-decomposition)
|
|
23
24
|
- [Permission Sets (grouped-by-tag)](#additional-permission-set-decomposition)
|
|
@@ -27,6 +28,8 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
|
|
|
27
28
|
- [Troubleshooting](#troubleshooting)
|
|
28
29
|
- [Hooks](#hooks)
|
|
29
30
|
- [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
|
|
31
|
+
- [splitTags grammar](#splittags-grammar)
|
|
32
|
+
- [multiLevel grammar](#multilevel-grammar)
|
|
30
33
|
- [Ignore Files](#ignore-files)
|
|
31
34
|
- [.forceignore](#forceignore)
|
|
32
35
|
- [.sfdecomposerignore](#sfdecomposerignore)
|
|
@@ -113,10 +116,11 @@ Salesforce’s built-in decomposition is limited. sf-decomposer gives admins and
|
|
|
113
116
|
|
|
114
117
|
## Commands
|
|
115
118
|
|
|
116
|
-
| Command | Description
|
|
117
|
-
| ------------------------- |
|
|
118
|
-
| `sf decomposer decompose` | Decompose metadata in package directories into smaller files.
|
|
119
|
-
| `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata.
|
|
119
|
+
| Command | Description |
|
|
120
|
+
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
121
|
+
| `sf decomposer decompose` | Decompose metadata in package directories into smaller files. |
|
|
122
|
+
| `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata. |
|
|
123
|
+
| `sf decomposer verify` | Round-trip check: decompose + recompose in a temp directory and diff the originals. |
|
|
120
124
|
|
|
121
125
|
### sf decomposer decompose
|
|
122
126
|
|
|
@@ -193,9 +197,49 @@ sf decomposer recompose -x "manifest/package.xml"
|
|
|
193
197
|
sf project deploy start -x "manifest/package.xml"
|
|
194
198
|
```
|
|
195
199
|
|
|
196
|
-
###
|
|
200
|
+
### sf decomposer verify
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
Non-destructive round-trip check: copies your package directories into a temp directory under your OS's `tmpdir()`, runs decompose then recompose there, and diffs the rebuilt parents against the originals using **structural XML equality** (sibling and attribute order are ignored). Exits non-zero on any drift; your working tree is never modified.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
USAGE
|
|
206
|
+
$ sf decomposer verify [-m <value>] [-x <value>] [-f <value>] [-i <value>] [-s <value>] [-p -c --json]
|
|
207
|
+
|
|
208
|
+
FLAGS
|
|
209
|
+
-m, --metadata-type=<value> Metadata suffix to verify (e.g. flow, labels). Repeatable. Optional when --manifest is provided.
|
|
210
|
+
-x, --manifest=<value> Path to a package.xml manifest. When provided, only the components listed in the manifest are verified.
|
|
211
|
+
-f, --format=<value> Output format used for the round-trip decompose: xml | yaml | json | json5 [default: xml]
|
|
212
|
+
-i, --ignore-package-directory=<value> Package directory to skip. Repeatable.
|
|
213
|
+
-s, --strategy=<value> unique-id | grouped-by-tag [default: unique-id]
|
|
214
|
+
-p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions.
|
|
215
|
+
-c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root, the same as `decompose --config`. [default: false]
|
|
216
|
+
|
|
217
|
+
GLOBAL FLAGS
|
|
218
|
+
--json Output as JSON.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to the intersection of the two.
|
|
222
|
+
|
|
223
|
+
**Examples**
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Verify two metadata types round-trip cleanly with defaults
|
|
227
|
+
sf decomposer verify -m "permissionset" -m "profile"
|
|
228
|
+
|
|
229
|
+
# Verify a different strategy + nested-perms split before committing the change
|
|
230
|
+
sf decomposer verify -m "permissionset" -s "grouped-by-tag" -p
|
|
231
|
+
|
|
232
|
+
# CI gate: verify just the components in a deploy manifest, using the repo-root config
|
|
233
|
+
sf decomposer verify -x "manifest/package.xml" --config
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Files where the **only** delta is sibling or attribute ordering are surfaced separately as informational notices ("Note: N file(s) round-tripped semantically but with sibling/attribute reordering") rather than as drift. This is safe — Salesforce treats metadata as order-agnostic and `config-disassembler` does not preserve original sibling order — but it tells you up front that committing the post-recompose output will produce a diff in git even though the metadata is functionally identical.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Manifest-scoped runs
|
|
241
|
+
|
|
242
|
+
The `-x` / `--manifest` flag is supported by every `sf decomposer` command (`decompose`, `recompose`, `verify`) and accepts any standard Salesforce `package.xml`, limiting the work to just the components it lists. This is especially useful for CI/CD pipelines that deploy a subset of metadata per change.
|
|
199
243
|
|
|
200
244
|
How it works:
|
|
201
245
|
|
|
@@ -407,15 +451,17 @@ By default, a single decompose run uses one format and one strategy across every
|
|
|
407
451
|
|
|
408
452
|
### What can be overridden
|
|
409
453
|
|
|
410
|
-
| Field | Notes
|
|
411
|
-
| ---------------------------- |
|
|
412
|
-
| `metadataTypes` | Optional (required if `components` is omitted). Array of metadata suffixes (same vocabulary as `--metadata-type` / `metadataSuffixes`). Each suffix may appear in at most one override.
|
|
413
|
-
| `components` | Optional (required if `metadataTypes` is omitted). Array of `<metadataSuffix>:<fullName>` keys (e.g. `permissionset:HR_Admin`, `report:MyFolder/MyReport`). Each component may appear in at most one override.
|
|
414
|
-
| `decomposedFormat` | `xml` \| `json` \| `json5` \| `yaml`.
|
|
415
|
-
| `strategy` | `unique-id` \| `grouped-by-tag`. Hard rules still win — `labels` and `loyaltyProgramSetup` are always treated as `unique-id`.
|
|
416
|
-
| `decomposeNestedPermissions` | Only applies to `permissionset` / `mutingpermissionset` with `grouped-by-tag`.
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
454
|
+
| Field | Notes |
|
|
455
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
456
|
+
| `metadataTypes` | Optional (required if `components` is omitted). Array of metadata suffixes (same vocabulary as `--metadata-type` / `metadataSuffixes`). Each suffix may appear in at most one override. |
|
|
457
|
+
| `components` | Optional (required if `metadataTypes` is omitted). Array of `<metadataSuffix>:<fullName>` keys (e.g. `permissionset:HR_Admin`, `report:MyFolder/MyReport`). Each component may appear in at most one override. |
|
|
458
|
+
| `decomposedFormat` | `xml` \| `json` \| `json5` \| `yaml`. |
|
|
459
|
+
| `strategy` | `unique-id` \| `grouped-by-tag`. Hard rules still win — `labels` and `loyaltyProgramSetup` are always treated as `unique-id`. |
|
|
460
|
+
| `decomposeNestedPermissions` | Only applies to `permissionset` / `mutingpermissionset` with `grouped-by-tag`. Sets a known-good `splitTags` default; ignored if `splitTags` is also set in the same scope. |
|
|
461
|
+
| `splitTags` | Custom `splitTags` spec for `grouped-by-tag` strategy. See [splitTags grammar](#splittags-grammar). Ignored when the resolved strategy is not `grouped-by-tag`. |
|
|
462
|
+
| `multiLevel` | One or more `multiLevel` specs for nested-array decomposition. Pass a string, a `string[]`, or a `;`-separated string. See [multiLevel grammar](#multilevel-grammar). When set, replaces the hardcoded `loyaltyProgramSetup` default for the targeted scope. |
|
|
463
|
+
| `prePurge` | Per-scope prePurge (decompose). Component-scope `prePurge` only purges the named component's decomposed directory. |
|
|
464
|
+
| `postPurge` | Per-scope postPurge (decompose: remove originals after decomposing). |
|
|
419
465
|
|
|
420
466
|
Run-scope options (`metadataSuffixes`, `manifest`, `ignorePackageDirectories`) are **not** valid inside an override; the plugin will throw if they are present.
|
|
421
467
|
|
|
@@ -439,6 +485,95 @@ For each component, each option is resolved independently in this order (highest
|
|
|
439
485
|
3. The run-wide value (CLI flag, hook config top-level field, or built-in default).
|
|
440
486
|
4. Hard plugin rules (e.g. `labels` and `loyaltyProgramSetup` forced to `unique-id`) override all of the above.
|
|
441
487
|
|
|
488
|
+
### splitTags grammar
|
|
489
|
+
|
|
490
|
+
`splitTags` lets you control how `grouped-by-tag` writes nested arrays for any metadata type. The plugin already applies a known-good default for permission sets when `decomposeNestedPermissions: true` is set; setting `splitTags` directly takes precedence and works for any metadata type.
|
|
491
|
+
|
|
492
|
+
**Spec:** Comma-separated rules. Each rule has 3 or 4 colon-separated parts:
|
|
493
|
+
|
|
494
|
+
- `<tag>:<mode>:<field>` — read array items from the top-level `<tag>`.
|
|
495
|
+
- `<tag>:<path>:<mode>:<field>` — read array items from the nested `<path>` (defaults to `<tag>`).
|
|
496
|
+
|
|
497
|
+
`<mode>` is one of:
|
|
498
|
+
|
|
499
|
+
- **`split`** — write one file per array item, named after the value of `<field>` on each item.
|
|
500
|
+
- **`group`** — group array items by the value of `<field>`, writing one file per group.
|
|
501
|
+
|
|
502
|
+
Each `<tag>` may appear at most once in a spec. The plugin validates the grammar at config-load time. Deeper checks (e.g. unknown tag names for the metadata type) are surfaced by the underlying disassembler crate at runtime.
|
|
503
|
+
|
|
504
|
+
#### splitTags cookbook
|
|
505
|
+
|
|
506
|
+
```json
|
|
507
|
+
"overrides": [
|
|
508
|
+
{
|
|
509
|
+
"metadataTypes": ["permissionset", "mutingpermissionset"],
|
|
510
|
+
"strategy": "grouped-by-tag",
|
|
511
|
+
"splitTags": "objectPermissions:split:object,fieldPermissions:group:field"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"metadataTypes": ["profile"],
|
|
515
|
+
"strategy": "grouped-by-tag",
|
|
516
|
+
"splitTags": "objectPermissions:split:object,fieldPermissions:group:field,layoutAssignments:group:layout"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"metadataTypes": ["flow"],
|
|
520
|
+
"strategy": "grouped-by-tag",
|
|
521
|
+
"splitTags": "actionCalls:split:name,decisions:split:name,assignments:split:name"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"metadataTypes": ["workflow"],
|
|
525
|
+
"strategy": "grouped-by-tag",
|
|
526
|
+
"splitTags": "rules:split:fullName,alerts:split:fullName,fieldUpdates:split:fullName,tasks:split:fullName"
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
> **Caveat:** When using `mode: split`, the chosen `<field>` must produce a unique value for every array item — otherwise two items would map to the same filename. If two items share a field value, prefer `mode: group` instead, which is designed for that case.
|
|
532
|
+
|
|
533
|
+
### multiLevel grammar
|
|
534
|
+
|
|
535
|
+
`multiLevel` enables a second decomposition pass on inner-level files for metadata types whose XML has deeply nested repeatable blocks (e.g. `loyaltyProgramSetup`'s `programProcesses → parameters → ...`, or a Bot's `botVersion → botDialogs → botSteps`). The plugin already applies a known-good default for `loyaltyProgramSetup` when running the `unique-id` strategy; setting `multiLevel` directly takes precedence and works for any metadata type.
|
|
536
|
+
|
|
537
|
+
**Spec:** Each rule has exactly 3 colon-separated parts (the third part is itself a comma-separated list):
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
<file_pattern>:<root_to_strip>:<unique_id_elements>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
- **`<file_pattern>`** — basename pattern that selects which inner-level files get the second decomposition pass (e.g. `programProcesses`).
|
|
544
|
+
- **`<root_to_strip>`** — XML root tag to strip from each matched file before splitting.
|
|
545
|
+
- **`<unique_id_elements>`** — comma-separated list of element names used to derive a stable filename for each inner-level item (e.g. `parameterName,ruleName`). The first element that resolves to a non-empty value wins.
|
|
546
|
+
|
|
547
|
+
A scope may target several nested sections by passing **multiple rules**. Three input shapes are supported:
|
|
548
|
+
|
|
549
|
+
- a single rule string (legacy, unchanged behaviour);
|
|
550
|
+
- a JSON `string[]` of rules (preferred — clearest intent, easiest to diff);
|
|
551
|
+
- a single `;`-separated string of rules (compact form, also accepted).
|
|
552
|
+
|
|
553
|
+
Within one scope, the `(file_pattern, root_to_strip)` pair must be unique across rules. The plugin validates the grammar at config-load time; deeper checks (whether a file pattern matches anything, whether the unique-id elements actually appear on the inner XML) are surfaced by the underlying disassembler crate at runtime.
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
"overrides": [
|
|
557
|
+
{
|
|
558
|
+
"metadataTypes": ["loyaltyProgramSetup"],
|
|
559
|
+
"multiLevel": "programProcesses:programProcesses:parameterName,ruleName"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"components": ["bot:Assessment_Bot"],
|
|
563
|
+
"multiLevel": [
|
|
564
|
+
"botDialogs:botDialogs:developerName",
|
|
565
|
+
"botSteps:botSteps:type"
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
> **Why one call:** Pass every rule for a given component in a single override. Sequential single-rule decompositions rewrite the on-disk `.multi_level.json` and only the last rule survives — so multi-rule scenarios must travel together.
|
|
572
|
+
|
|
573
|
+
> **Tip:** Use [`sf decomposer verify`](#sf-decomposer-verify) to non-destructively confirm a new override config still round-trips before committing it.
|
|
574
|
+
|
|
575
|
+
> **Tip:** See the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for end-to-end recipes for Bots, Flexipages, Layouts, and other deeply-nested metadata.
|
|
576
|
+
|
|
442
577
|
### Opting in from the CLI
|
|
443
578
|
|
|
444
579
|
CLI users can opt into overrides on `decompose` with the boolean `--config` (`-c`) flag. When set, the plugin reads `.sfdecomposer.config.json` from the repo root (the nearest ancestor directory that contains `sfdx-project.json`):
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SfCommand } from '@salesforce/sf-plugins-core';
|
|
2
|
+
import { VerifyResult } from '../../helpers/types.js';
|
|
3
|
+
export default class DecomposerVerify extends SfCommand<VerifyResult> {
|
|
4
|
+
static readonly summary: string;
|
|
5
|
+
static readonly description: string;
|
|
6
|
+
static readonly examples: string[];
|
|
7
|
+
static readonly flags: {
|
|
8
|
+
'metadata-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
manifest: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'ignore-package-directory': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
strategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'decompose-nested-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
config: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<VerifyResult>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
|
|
3
|
+
import { Messages } from '@salesforce/core';
|
|
4
|
+
import { DECOMPOSED_FILE_TYPES, DECOMPOSED_STRATEGIES } from '../../helpers/constants.js';
|
|
5
|
+
import { verifyMetadataTypes } from '../../core/verifyMetadataTypes.js';
|
|
6
|
+
import { loadOverridesFromConfig, resolveDefaultConfigPath } from '../../helpers/configOverrides.js';
|
|
7
|
+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
|
|
8
|
+
const messages = Messages.loadMessages('sf-decomposer', 'decomposer.verify');
|
|
9
|
+
export default class DecomposerVerify extends SfCommand {
|
|
10
|
+
static summary = messages.getMessage('summary');
|
|
11
|
+
static description = messages.getMessage('description');
|
|
12
|
+
static examples = messages.getMessages('examples');
|
|
13
|
+
static flags = {
|
|
14
|
+
'metadata-type': Flags.string({
|
|
15
|
+
summary: messages.getMessage('flags.metadata-type.summary'),
|
|
16
|
+
char: 'm',
|
|
17
|
+
multiple: true,
|
|
18
|
+
required: false,
|
|
19
|
+
}),
|
|
20
|
+
manifest: Flags.file({
|
|
21
|
+
summary: messages.getMessage('flags.manifest.summary'),
|
|
22
|
+
char: 'x',
|
|
23
|
+
required: false,
|
|
24
|
+
exists: true,
|
|
25
|
+
}),
|
|
26
|
+
format: Flags.string({
|
|
27
|
+
summary: messages.getMessage('flags.format.summary'),
|
|
28
|
+
char: 'f',
|
|
29
|
+
required: true,
|
|
30
|
+
multiple: false,
|
|
31
|
+
default: 'xml',
|
|
32
|
+
options: DECOMPOSED_FILE_TYPES,
|
|
33
|
+
}),
|
|
34
|
+
'ignore-package-directory': Flags.directory({
|
|
35
|
+
summary: messages.getMessage('flags.ignore-package-directory.summary'),
|
|
36
|
+
char: 'i',
|
|
37
|
+
required: false,
|
|
38
|
+
multiple: true,
|
|
39
|
+
}),
|
|
40
|
+
strategy: Flags.string({
|
|
41
|
+
summary: messages.getMessage('flags.strategy.summary'),
|
|
42
|
+
char: 's',
|
|
43
|
+
required: true,
|
|
44
|
+
multiple: false,
|
|
45
|
+
default: 'unique-id',
|
|
46
|
+
options: DECOMPOSED_STRATEGIES,
|
|
47
|
+
}),
|
|
48
|
+
'decompose-nested-permissions': Flags.boolean({
|
|
49
|
+
summary: messages.getMessage('flags.decompose-nested-permissions.summary'),
|
|
50
|
+
char: 'p',
|
|
51
|
+
required: false,
|
|
52
|
+
default: false,
|
|
53
|
+
}),
|
|
54
|
+
config: Flags.boolean({
|
|
55
|
+
summary: messages.getMessage('flags.config.summary'),
|
|
56
|
+
char: 'c',
|
|
57
|
+
required: false,
|
|
58
|
+
default: false,
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
async run() {
|
|
62
|
+
const { flags } = await this.parse(DecomposerVerify);
|
|
63
|
+
if (!flags['metadata-type'] && !flags['manifest']) {
|
|
64
|
+
throw messages.createError('error.missingMetadataOrManifest');
|
|
65
|
+
}
|
|
66
|
+
const overrides = flags['config'] ? await loadOverridesFromConfig(await resolveDefaultConfigPath()) : undefined;
|
|
67
|
+
const result = await verifyMetadataTypes({
|
|
68
|
+
metadataTypes: flags['metadata-type'],
|
|
69
|
+
format: flags['format'],
|
|
70
|
+
ignoreDirs: flags['ignore-package-directory'],
|
|
71
|
+
strategy: flags['strategy'],
|
|
72
|
+
decomposeNestedPerms: flags['decompose-nested-permissions'],
|
|
73
|
+
manifest: flags['manifest'],
|
|
74
|
+
overrides,
|
|
75
|
+
log: this.log.bind(this),
|
|
76
|
+
});
|
|
77
|
+
if (result.drift.length > 0) {
|
|
78
|
+
throw messages.createError('error.driftDetected', [String(result.drift.length)]);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/commands/decomposer/verify.ts"],"names":[],"mappings":"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"}
|