sf-decomposer 6.24.1 → 6.25.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 +7 -0
- package/HANDBOOK.md +24 -24
- package/README.md +176 -180
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
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.25.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.24.1...v6.25.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **metadata:** bump @salesforce/source-deploy-retrieve ([#467](https://github.com/mcarvin8/sf-decomposer/issues/467)) ([30193f2](https://github.com/mcarvin8/sf-decomposer/commit/30193f2b1a0235e77653d05de28ee76fb0971e92))
|
|
14
|
+
|
|
8
15
|
## [6.24.1](https://github.com/mcarvin8/sf-decomposer/compare/v6.24.0...v6.24.1) (2026-05-26)
|
|
9
16
|
|
|
10
17
|
|
package/HANDBOOK.md
CHANGED
|
@@ -11,12 +11,12 @@ If you want the underlying option grammar instead of recipes, see the [main READ
|
|
|
11
11
|
## Contents
|
|
12
12
|
|
|
13
13
|
- [Choosing a strategy](#choosing-a-strategy)
|
|
14
|
+
- [Common pitfalls](#common-pitfalls)
|
|
14
15
|
- [Bots (Agentforce and Einstein)](#bots-agentforce-and-einstein)
|
|
15
16
|
- [Flexipages (Lightning App / Record / Home pages)](#flexipages-lightning-app--record--home-pages)
|
|
16
17
|
- [Layouts (page layouts)](#layouts-page-layouts)
|
|
17
18
|
- [Other deeply-nested types](#other-deeply-nested-types)
|
|
18
19
|
- [The verification workflow](#the-verification-workflow)
|
|
19
|
-
- [Common pitfalls](#common-pitfalls)
|
|
20
20
|
|
|
21
21
|
## Choosing a strategy
|
|
22
22
|
|
|
@@ -44,6 +44,26 @@ The full registry lives in [`src/metadata/multiLevelDefaults.ts`](./src/metadata
|
|
|
44
44
|
|
|
45
45
|
Everything else in this handbook is opt-in.
|
|
46
46
|
|
|
47
|
+
## Common pitfalls
|
|
48
|
+
|
|
49
|
+
**1. "I added two `multiLevel` rules but only one survived."**
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
**2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
|
|
53
|
+
On `config-disassembler` Rust ≥ 0.5.0 / Node ≥ 1.3.0 this should not happen — sibling collisions are written to per-element SHA-256 shards and surfaced as a `WARN` (see pitfall #5), not silently overwritten. If you do see a shrunken recomposed file on a current build, treat it as a regression worth capturing as a fixture.
|
|
54
|
+
|
|
55
|
+
**3. "Component-scope override fields look ignored."**
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
**4. "Reassembly removed my decomposed directory even though I didn't pass `postPurge`."**
|
|
59
|
+
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`.
|
|
60
|
+
|
|
61
|
+
**5. "Decompose succeeded but my decomposed files all have hash names."**
|
|
62
|
+
There are two distinct causes; run the decompose under `RUST_LOG=warn` to tell them apart:
|
|
63
|
+
|
|
64
|
+
- **No `WARN` line.** 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.
|
|
65
|
+
- **A `WARN` line of the form `uniqueIdElements collision: <parentTag> id "X" matched N sibling elements`.** The configured key is too narrow — multiple siblings legitimately share the same value, so the collision detector falls back to per-element SHA-256 hashes for that group rather than overwrite. Add a tiebreaker to `unique_id_elements` (e.g. a compound like `name+recordType`) and re-decompose with `prePurge: true`.
|
|
66
|
+
|
|
47
67
|
## Bots (Agentforce and Einstein)
|
|
48
68
|
|
|
49
69
|
**Why this is hard.** Agentforce and Einstein bots both ship as `.bot-meta.xml`, but their internals diverge:
|
|
@@ -263,37 +283,17 @@ Always verify a new override before committing it:
|
|
|
263
283
|
git stash --include-untracked
|
|
264
284
|
|
|
265
285
|
# 2. Decompose with the new override.
|
|
266
|
-
sf decomposer decompose -
|
|
286
|
+
sf decomposer decompose -m "bot" --config
|
|
267
287
|
|
|
268
288
|
# 3. Recompose back from the decomposed tree.
|
|
269
|
-
sf decomposer recompose -
|
|
289
|
+
sf decomposer recompose -m "bot"
|
|
270
290
|
|
|
271
291
|
# 4. Check the round-trip didn't drift.
|
|
272
|
-
sf decomposer verify -
|
|
292
|
+
sf decomposer verify -m "bot" --config
|
|
273
293
|
```
|
|
274
294
|
|
|
275
295
|
`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.
|
|
276
296
|
|
|
277
|
-
## Common pitfalls
|
|
278
|
-
|
|
279
|
-
**1. "I added two `multiLevel` rules but only one survived."**
|
|
280
|
-
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.
|
|
281
|
-
|
|
282
|
-
**2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
|
|
283
|
-
On `config-disassembler` Rust ≥ 0.5.0 / Node ≥ 1.3.0 this should not happen — sibling collisions are written to per-element SHA-256 shards and surfaced as a `WARN` (see pitfall #5), not silently overwritten. If you do see a shrunken recomposed file on a current build, treat it as a regression worth capturing as a fixture.
|
|
284
|
-
|
|
285
|
-
**3. "Component-scope override fields look ignored."**
|
|
286
|
-
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.
|
|
287
|
-
|
|
288
|
-
**4. "Reassembly removed my decomposed directory even though I didn't pass `postPurge`."**
|
|
289
|
-
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`.
|
|
290
|
-
|
|
291
|
-
**5. "Decompose succeeded but my decomposed files all have hash names."**
|
|
292
|
-
There are two distinct causes; run the decompose under `RUST_LOG=warn` to tell them apart:
|
|
293
|
-
|
|
294
|
-
- **No `WARN` line.** 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.
|
|
295
|
-
- **A `WARN` line of the form `uniqueIdElements collision: <parentTag> id "X" matched N sibling elements`.** The configured key is too narrow — multiple siblings legitimately share the same value, so the collision detector falls back to per-element SHA-256 hashes for that group rather than overwrite. Add a tiebreaker to `unique_id_elements` (e.g. a compound like `name+recordType`) and re-decompose with `prePurge: true`.
|
|
296
|
-
|
|
297
297
|
---
|
|
298
298
|
|
|
299
299
|
When in doubt: write the override, run `verify`, read the diff. Every recipe in this handbook was derived that way.
|
package/README.md
CHANGED
|
@@ -14,93 +14,117 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
|
|
|
14
14
|
<details>
|
|
15
15
|
<summary>Table of Contents</summary>
|
|
16
16
|
|
|
17
|
-
- [
|
|
18
|
-
- [Requirements](#requirements)
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
17
|
+
- [Setup](#setup)
|
|
18
|
+
- [1. Requirements](#1-requirements)
|
|
19
|
+
- [2. Install the Plugin](#2-install-the-plugin)
|
|
20
|
+
- [3. Configure .forceignore](#3-configure-forceignore)
|
|
21
|
+
- [4. Configure Hooks](#4-configure-hooks-recommended)
|
|
22
|
+
- [Daily Workflow](#daily-workflow)
|
|
23
|
+
- [Reference](#reference)
|
|
24
|
+
- [Commands](#commands)
|
|
25
|
+
- [Decompose Strategies](#decompose-strategies)
|
|
26
|
+
- [Supported Metadata](#supported-metadata)
|
|
27
|
+
- [Manifest-scoped Runs](#manifest-scoped-runs)
|
|
28
|
+
- [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
|
|
29
|
+
- [Ignore Files](#ignore-files)
|
|
30
|
+
- [Troubleshooting](#troubleshooting)
|
|
31
|
+
- [Built With](#built-with)
|
|
30
32
|
- [Contributing](#contributing)
|
|
31
33
|
- [License](#license)
|
|
34
|
+
|
|
32
35
|
</details>
|
|
33
36
|
|
|
34
37
|
---
|
|
35
38
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
1. **Install the plugin**
|
|
39
|
+
## Setup
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
sf plugins install sf-decomposer@x.y.z
|
|
42
|
-
```
|
|
41
|
+
Complete these steps once per project. After setup, see [Daily Workflow](#daily-workflow).
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
### 1. Requirements
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
- [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli) (`sf`) installed
|
|
46
|
+
- Node.js 20.x or later
|
|
47
|
+
- A Salesforce DX project with `sfdx-project.json` and package directories
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
sf decomposer decompose -m "flow" -m "labels" --postpurge
|
|
50
|
-
```
|
|
49
|
+
**Supported Platforms**
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
sf-decomposer depends on [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node), which ships prebuilt native binaries as platform-specific optional npm packages — your package manager installs only the one matching your `os` / `cpu` / `libc`:
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
| Platform | Architectures |
|
|
54
|
+
| ----------- | ------------------------------------ |
|
|
55
|
+
| **macOS** | x64 (Intel), arm64 (Apple Silicon) |
|
|
56
|
+
| **Linux** | x64 (gnu + musl), arm64 (gnu + musl) |
|
|
57
|
+
| **Windows** | x64, arm64, ia32 |
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
If your platform or architecture is not listed, open an [issue](https://github.com/mcarvin8/sf-decomposer/issues).
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
### 2. Install the Plugin
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
63
|
+
```bash
|
|
64
|
+
sf plugins install sf-decomposer@x.y.z
|
|
65
|
+
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
### 3. Configure .forceignore
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
**Required.** The Salesforce CLI must ignore decomposed files or `sf` commands will fail. Configure this before running any decompose or retrieve commands.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
Copy the [sample .forceignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.forceignore) into your project root and adjust the extension patterns for your chosen format (`.xml`, `.json`, `.yaml`, etc.).
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
- Node.js 20.x or later
|
|
74
|
-
- A Salesforce DX project with `sfdx-project.json` and package directories
|
|
73
|
+
### 4. Configure Hooks (Recommended)
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
Hooks auto-decompose after `sf project retrieve start` and auto-recompose before `sf project deploy start` / `validate` — eliminating manual steps entirely.
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
Add `.sfdecomposer.config.json` to your project root. Copy and customize one of the sample configs:
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| **macOS** | x64 (Intel), arm64 (Apple Silicon) |
|
|
83
|
-
| **Linux** | x64 (gnu + musl), arm64 (gnu + musl) |
|
|
84
|
-
| **Windows** | x64, arm64, ia32 |
|
|
79
|
+
- [Basic sample](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json) — one format and strategy for all types
|
|
80
|
+
- [Sample with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) — vary format/strategy per metadata type or component
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
| Option | Required | Description |
|
|
83
|
+
| ---------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, run is scoped to the intersection. |
|
|
85
|
+
| `manifest` | Conditional | Path (relative to project root) to a `package.xml` manifest. When set, only listed components are decomposed/recomposed. |
|
|
86
|
+
| `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
|
|
87
|
+
| `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
|
|
88
|
+
| `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
|
|
89
|
+
| `decomposedFormat` | No | `xml`, `json`, `json5`, or `yaml` (default: xml). |
|
|
90
|
+
| `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
|
|
91
|
+
| `decomposeNestedPermissions` | No | With `grouped-by-tag`, set `true` to further decompose permission set and muting permission set object/field permissions. |
|
|
92
|
+
| `overrides` | No | Array of per-type and/or per-component overrides. See [Per-Type & Per-Component Overrides](#per-type--per-component-overrides). |
|
|
87
93
|
|
|
88
94
|
---
|
|
89
95
|
|
|
90
|
-
##
|
|
96
|
+
## Daily Workflow
|
|
97
|
+
|
|
98
|
+
**With hooks configured** (recommended):
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
```
|
|
101
|
+
retrieve → auto-decomposes → review & commit → deploy → auto-recomposes
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
sf project retrieve start # hooks decompose automatically
|
|
106
|
+
git add . && git commit -m "..." # commit decomposed files
|
|
107
|
+
sf project deploy start # hooks recompose automatically
|
|
108
|
+
```
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
**Without hooks** (manual):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# After retrieve: decompose
|
|
114
|
+
sf decomposer decompose -m "flow" -m "labels" --postpurge
|
|
115
|
+
|
|
116
|
+
# Before deploy: recompose, then deploy
|
|
117
|
+
sf decomposer recompose -m "flow" -m "labels"
|
|
118
|
+
sf project deploy start
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Pass `-x manifest/package.xml` to both `decompose` and `recompose` (and `deploy`) to scope a run to just the components in a deploy manifest.
|
|
100
122
|
|
|
101
123
|
---
|
|
102
124
|
|
|
103
|
-
##
|
|
125
|
+
## Reference
|
|
126
|
+
|
|
127
|
+
### Commands
|
|
104
128
|
|
|
105
129
|
| Command | Description |
|
|
106
130
|
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
@@ -108,7 +132,7 @@ Salesforce's built-in decomposition is limited. sf-decomposer gives admins and d
|
|
|
108
132
|
| `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata. |
|
|
109
133
|
| `sf decomposer verify` | Round-trip check: decompose + recompose in a temp directory and diff the originals. |
|
|
110
134
|
|
|
111
|
-
|
|
135
|
+
#### sf decomposer decompose
|
|
112
136
|
|
|
113
137
|
Decomposes metadata in all local package directories (from `sfdx-project.json`) into smaller files.
|
|
114
138
|
|
|
@@ -125,13 +149,13 @@ FLAGS
|
|
|
125
149
|
--prepurge Remove existing decomposed files before decomposing [default: false]
|
|
126
150
|
--postpurge Remove original metadata files after decomposing [default: false]
|
|
127
151
|
-p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions
|
|
128
|
-
-c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed.
|
|
152
|
+
-c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. [default: false]
|
|
129
153
|
|
|
130
154
|
GLOBAL FLAGS
|
|
131
155
|
--json Output as JSON.
|
|
132
156
|
```
|
|
133
157
|
|
|
134
|
-
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to
|
|
158
|
+
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
|
|
135
159
|
|
|
136
160
|
**Examples**
|
|
137
161
|
|
|
@@ -152,7 +176,7 @@ sf decomposer decompose -x "manifest/package.xml" --prepurge
|
|
|
152
176
|
sf decomposer decompose -x "manifest/package.xml" -m "permissionset"
|
|
153
177
|
```
|
|
154
178
|
|
|
155
|
-
|
|
179
|
+
#### sf decomposer recompose
|
|
156
180
|
|
|
157
181
|
Recomposes decomposed files into deployment-compatible metadata.
|
|
158
182
|
|
|
@@ -170,7 +194,7 @@ GLOBAL FLAGS
|
|
|
170
194
|
--json Output as JSON.
|
|
171
195
|
```
|
|
172
196
|
|
|
173
|
-
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to
|
|
197
|
+
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
|
|
174
198
|
|
|
175
199
|
**Examples**
|
|
176
200
|
|
|
@@ -183,7 +207,7 @@ sf decomposer recompose -x "manifest/package.xml"
|
|
|
183
207
|
sf project deploy start -x "manifest/package.xml"
|
|
184
208
|
```
|
|
185
209
|
|
|
186
|
-
|
|
210
|
+
#### sf decomposer verify
|
|
187
211
|
|
|
188
212
|
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.
|
|
189
213
|
|
|
@@ -198,13 +222,13 @@ FLAGS
|
|
|
198
222
|
-i, --ignore-package-directory=<value> Package directory to skip. Repeatable.
|
|
199
223
|
-s, --strategy=<value> unique-id | grouped-by-tag [default: unique-id]
|
|
200
224
|
-p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions.
|
|
201
|
-
-c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root
|
|
225
|
+
-c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. [default: false]
|
|
202
226
|
|
|
203
227
|
GLOBAL FLAGS
|
|
204
228
|
--json Output as JSON.
|
|
205
229
|
```
|
|
206
230
|
|
|
207
|
-
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to
|
|
231
|
+
> At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
|
|
208
232
|
|
|
209
233
|
**Examples**
|
|
210
234
|
|
|
@@ -219,44 +243,11 @@ sf decomposer verify -m "permissionset" -s "grouped-by-tag" -p
|
|
|
219
243
|
sf decomposer verify -x "manifest/package.xml" --config
|
|
220
244
|
```
|
|
221
245
|
|
|
222
|
-
Files whose **only** delta is sibling or attribute ordering are reported as informational notices, not drift. Salesforce treats metadata as order-agnostic, so the deploy is safe — the notice
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## Manifest-scoped runs
|
|
227
|
-
|
|
228
|
-
`-x` / `--manifest` is supported by every `sf decomposer` command and accepts the same `package.xml` you pass to `sf project deploy start -x`. Only the listed components are decomposed/recomposed; everything else is left alone.
|
|
229
|
-
|
|
230
|
-
- Wildcards (`<members>*</members>`) expand against your local source.
|
|
231
|
-
- Folder members (e.g. `MyFolder/MyReport`) resolve by walking the folder.
|
|
232
|
-
- Types the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning, so the same manifest can drive both deploys and decomposer runs.
|
|
233
|
-
- If both `--metadata-type` and `--manifest` are supplied, the run is scoped to the intersection.
|
|
234
|
-
|
|
235
|
-
Example manifest:
|
|
236
|
-
|
|
237
|
-
```xml
|
|
238
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
239
|
-
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
240
|
-
<types>
|
|
241
|
-
<members>HR_Admin</members>
|
|
242
|
-
<name>PermissionSet</name>
|
|
243
|
-
</types>
|
|
244
|
-
<types>
|
|
245
|
-
<members>Case</members>
|
|
246
|
-
<name>Workflow</name>
|
|
247
|
-
</types>
|
|
248
|
-
<version>58.0</version>
|
|
249
|
-
</Package>
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
```bash
|
|
253
|
-
sf decomposer recompose -x "manifest/package.xml"
|
|
254
|
-
sf project deploy start -x "manifest/package.xml"
|
|
255
|
-
```
|
|
246
|
+
Files whose **only** delta is sibling or attribute ordering are reported as informational notices, not drift. Salesforce treats metadata as order-agnostic, so the deploy is safe — the notice warns that committing the post-recompose output will show a git diff even though the metadata is functionally identical.
|
|
256
247
|
|
|
257
248
|
---
|
|
258
249
|
|
|
259
|
-
|
|
250
|
+
### Decompose Strategies
|
|
260
251
|
|
|
261
252
|
> **Tip:** A single decompose run can mix strategies and formats across metadata types — and even across components within the same type — through the `overrides` array (see [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)). Recompose is deterministic from the on-disk sidecar, so any combination round-trips. When switching strategies for an existing component, pass `--prepurge` (or set `prePurge: true`) so leftover files from the previous strategy are removed before the new ones are written.
|
|
262
253
|
|
|
@@ -306,16 +297,16 @@ permissionsets/
|
|
|
306
297
|
└── userPermissions.xml
|
|
307
298
|
```
|
|
308
299
|
|
|
309
|
-
|
|
300
|
+
#### Filename safety (unique-id)
|
|
310
301
|
|
|
311
302
|
Two safety nets apply automatically to every shard filename emitted by the **unique-id** strategy. Neither requires configuration:
|
|
312
303
|
|
|
313
304
|
- **Path-segment sanitization (silent).** Characters illegal or reserved on at least one supported filesystem — path separators (`/`, `\`), Windows-reserved chars (`:`, `*`, `?`, `"`, `<`, `>`, `|`), and ASCII control bytes — are replaced with `_`; trailing `.` and spaces are stripped. Sanitized filenames are byte-stable across platforms.
|
|
314
305
|
- **Sibling-collision fallback (emits `WARN`).** When two or more siblings of the same parent tag would resolve to the same filename (the configured unique-id elements are too narrow, or sanitization folded two distinct values together), every sibling in the colliding group is written to its own per-element SHA-256 shard instead. No row is silently overwritten.
|
|
315
306
|
|
|
316
|
-
If you see a hash-named shard and want to know whether it came from a collision (vs.
|
|
307
|
+
If you see a hash-named shard and want to know whether it came from a collision (vs. a missing UID), set `RUST_LOG=warn` and rerun — see [Rust crate logging](#xml-disassemble-output-rust-crate).
|
|
317
308
|
|
|
318
|
-
|
|
309
|
+
#### Custom Labels Decomposition
|
|
319
310
|
|
|
320
311
|
Custom labels are always decomposed with `unique-id` (grouped-by-tag would be a no-op since every element shares the same tag). Each label is written to its own file:
|
|
321
312
|
|
|
@@ -326,14 +317,14 @@ labels/
|
|
|
326
317
|
└── quoteManual.label-meta.xml
|
|
327
318
|
```
|
|
328
319
|
|
|
329
|
-
|
|
320
|
+
#### Additional Permission Set Decomposition
|
|
330
321
|
|
|
331
322
|
With **grouped-by-tag**, use `--decompose-nested-permissions` (`-p`) to further decompose permission sets and muting permission sets:
|
|
332
323
|
|
|
333
324
|
- Write each `<objectPermissions>` to its own file under `objectPermissions/`.
|
|
334
325
|
- Group `<fieldPermissions>` by object under `fieldPermissions/`.
|
|
335
326
|
|
|
336
|
-
Similar to Salesforce
|
|
327
|
+
Similar to Salesforce's `decomposePermissionSetBeta2`, with more control and format options. Muting permission sets extend the permission set metadata type and support the same decomposition.
|
|
337
328
|
|
|
338
329
|
```bash
|
|
339
330
|
sf decomposer decompose -m "permissionset" -s "grouped-by-tag" -p
|
|
@@ -357,7 +348,7 @@ permissionsets/
|
|
|
357
348
|
└── Job_Request__c.objectPermissions-meta.xml
|
|
358
349
|
```
|
|
359
350
|
|
|
360
|
-
|
|
351
|
+
#### Loyalty Program Setup Decomposition
|
|
361
352
|
|
|
362
353
|
`loyaltyProgramSetup` is always decomposed with `unique-id`, with a built-in `multiLevel` default that splits `<programProcesses>` into per-process folders containing per-`<parameters>` / per-`<rules>` files.
|
|
363
354
|
|
|
@@ -386,15 +377,15 @@ loyaltyProgramSetups/
|
|
|
386
377
|
└── ...
|
|
387
378
|
```
|
|
388
379
|
|
|
389
|
-
> **Tip:** This three-level layout (`programProcesses` → `parameters`/`rules`) is
|
|
380
|
+
> **Tip:** This three-level layout (`programProcesses` → `parameters`/`rules`) is the multi-level decomposition pattern. The same pattern powers Bots, Flexipages, and Layouts via opt-in `multiLevel` overrides — see the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for those recipes.
|
|
390
381
|
|
|
391
382
|
---
|
|
392
383
|
|
|
393
|
-
|
|
384
|
+
### Supported Metadata
|
|
394
385
|
|
|
395
|
-
All parent metadata types from this plugin
|
|
386
|
+
All parent metadata types from this plugin's version of **@salesforce/source-deploy-retrieve** (SDR) are supported, except where noted below.
|
|
396
387
|
|
|
397
|
-
Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDR
|
|
388
|
+
Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDR's metadataRegistry.json](https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json), or infer from the file name: `*.{suffix}-meta.xml`.
|
|
398
389
|
|
|
399
390
|
| Metadata Type | CLI value | Notes |
|
|
400
391
|
| --------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -409,9 +400,9 @@ Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDR’s metadat
|
|
|
409
400
|
| Marketing App Extension | `marketingappextension` | |
|
|
410
401
|
| Loyalty Program Setup | `loyaltyProgramSetup` | Only `unique-id` strategy supported; `grouped-by-tag` is overridden. Automatically decomposed further (see [Loyalty Program Setup](#loyalty-program-setup-decomposition)). |
|
|
411
402
|
|
|
412
|
-
|
|
403
|
+
For a comprehensive breakdown of supported, leaf-only, and unsupported metadata types — including multi-level decomposition patterns, Salesforce native decomposition conflicts, and adapter strategy limitations — see [**METADATA_SUPPORT.md**](./METADATA_SUPPORT.md).
|
|
413
404
|
|
|
414
|
-
|
|
405
|
+
#### Exceptions
|
|
415
406
|
|
|
416
407
|
| Situation | Message |
|
|
417
408
|
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -422,71 +413,40 @@ The supported metadata table above provides a quick reference for some types. Fo
|
|
|
422
413
|
|
|
423
414
|
---
|
|
424
415
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
### Missing sfdx-project.json
|
|
416
|
+
### Manifest-scoped Runs
|
|
428
417
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
```
|
|
432
|
-
Error (1): sfdx-project.json not found in any parent directory.
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Package Directories Not Found for Given Metadata Type
|
|
418
|
+
`-x` / `--manifest` is supported by every `sf decomposer` command and accepts the same `package.xml` you pass to `sf project deploy start -x`. Only the listed components are decomposed/recomposed; everything else is left alone.
|
|
436
419
|
|
|
437
|
-
|
|
420
|
+
- Wildcards (`<members>*</members>`) expand against your local source.
|
|
421
|
+
- Folder members (e.g. `MyFolder/MyReport`) resolve by walking the folder.
|
|
422
|
+
- Types the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning, so the same manifest can drive both deploys and decomposer runs.
|
|
423
|
+
- If both `--metadata-type` and `--manifest` are supplied, the run is scoped to their intersection.
|
|
438
424
|
|
|
439
|
-
|
|
425
|
+
Example manifest:
|
|
440
426
|
|
|
427
|
+
```xml
|
|
428
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
429
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
430
|
+
<types>
|
|
431
|
+
<members>HR_Admin</members>
|
|
432
|
+
<name>PermissionSet</name>
|
|
433
|
+
</types>
|
|
434
|
+
<types>
|
|
435
|
+
<members>Case</members>
|
|
436
|
+
<name>Workflow</name>
|
|
437
|
+
</types>
|
|
438
|
+
<version>58.0</version>
|
|
439
|
+
</Package>
|
|
441
440
|
```
|
|
442
|
-
No directories named ${metadataTypeEntry.directoryName} were found in any package directory.
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
For example, if you attempt to decompose Custom Labels but none of your package directories contain a "labels" folder, the plugin will throw this error.
|
|
446
441
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
| Level | What it covers |
|
|
452
|
-
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
-
| `RUST_LOG=error` | Default. Parse errors and skipped files (leaf-only XML — primitives only, nothing to decompose). |
|
|
454
|
-
| `RUST_LOG=warn` | Adds [sibling-collision fallback](#filename-safety-unique-id) signals — one line per colliding group (parent tag, collided id, sibling count). **Recommended in CI** when shipping overrides. |
|
|
455
|
-
|
|
456
|
-
Example `WARN` (CustomApplication where four `actionOverrides` siblings shared the action name `View`):
|
|
457
|
-
|
|
458
|
-
```
|
|
459
|
-
[2026-05-04T15:21:09Z WARN config_disassembler::xml::builders::build_disassembled_files]
|
|
460
|
-
uniqueIdElements collision: <actionOverrides> id "View" matched 4 sibling elements;
|
|
461
|
-
falling back to SHA-256 content hashes for the colliding group.
|
|
462
|
-
Consider adding more discriminating fields to uniqueIdElements for this metadata type.
|
|
442
|
+
```bash
|
|
443
|
+
sf decomposer recompose -x "manifest/package.xml"
|
|
444
|
+
sf project deploy start -x "manifest/package.xml"
|
|
463
445
|
```
|
|
464
446
|
|
|
465
447
|
---
|
|
466
448
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
Put **.sfdecomposer.config.json** in the project root to auto-decompose after `sf project retrieve start` and auto-recompose before `sf project deploy start` / `validate`.
|
|
470
|
-
|
|
471
|
-
> Configure [.forceignore](#forceignore) first — the Salesforce CLI must ignore decomposed files or `sf` commands can fail.
|
|
472
|
-
|
|
473
|
-
Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy per metadata type or component.
|
|
474
|
-
|
|
475
|
-
| Option | Required | Description |
|
|
476
|
-
| ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
477
|
-
| `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, the run is scoped to the intersection. |
|
|
478
|
-
| `manifest` | Conditional | Path (relative to the project root) to a `package.xml` manifest. When set, only the components listed in the manifest are decomposed/recomposed. See `-x` above. |
|
|
479
|
-
| `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
|
|
480
|
-
| `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
|
|
481
|
-
| `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
|
|
482
|
-
| `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
|
|
483
|
-
| `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
|
|
484
|
-
| `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
|
|
485
|
-
| `overrides` | No | Array of per-type and/or per-component overrides for `decomposedFormat`, `strategy`, `decomposeNestedPermissions`, `prePurge`, and `postPurge`. See [Per-Type & Per-Component Overrides](#per-type--per-component-overrides). |
|
|
486
|
-
|
|
487
|
-
---
|
|
488
|
-
|
|
489
|
-
## Per-Type & Per-Component Overrides
|
|
449
|
+
### Per-Type & Per-Component Overrides
|
|
490
450
|
|
|
491
451
|
Overrides apply to **decompose only**. Recompose is a deterministic round-trip — it auto-detects format from the on-disk files and does not depend on strategy — so it ignores the `overrides` array.
|
|
492
452
|
|
|
@@ -516,7 +476,7 @@ By default, a single decompose run uses one format and one strategy across every
|
|
|
516
476
|
}
|
|
517
477
|
```
|
|
518
478
|
|
|
519
|
-
|
|
479
|
+
#### What can be overridden
|
|
520
480
|
|
|
521
481
|
| Field | Notes |
|
|
522
482
|
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -543,7 +503,7 @@ The `<fullName>` part of a component key is the SDR fullName for the component,
|
|
|
543
503
|
|
|
544
504
|
Component overrides are not a filter. If `--metadata` / `metadataSuffixes` includes `permissionset`, every permission set is still decomposed; the override only changes how the named ones are decomposed. Use `--manifest` / the hook's `manifest` field if you want to scope the run itself to a subset of components.
|
|
545
505
|
|
|
546
|
-
|
|
506
|
+
#### Precedence
|
|
547
507
|
|
|
548
508
|
For each component, each option is resolved independently in this order (highest first):
|
|
549
509
|
|
|
@@ -552,7 +512,7 @@ For each component, each option is resolved independently in this order (highest
|
|
|
552
512
|
3. The run-wide value (CLI flag, hook config top-level field, or built-in default).
|
|
553
513
|
4. Hard plugin rules (e.g. `labels` and `loyaltyProgramSetup` forced to `unique-id`) override all of the above.
|
|
554
514
|
|
|
555
|
-
|
|
515
|
+
#### splitTags grammar
|
|
556
516
|
|
|
557
517
|
`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.
|
|
558
518
|
|
|
@@ -589,7 +549,7 @@ Each `<tag>` may appear at most once in a spec. The plugin validates the grammar
|
|
|
589
549
|
|
|
590
550
|
See the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for additional `splitTags` and `multiLevel` recipes (flows, workflows, layouts, flexipages, bots).
|
|
591
551
|
|
|
592
|
-
|
|
552
|
+
#### multiLevel grammar
|
|
593
553
|
|
|
594
554
|
`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.
|
|
595
555
|
|
|
@@ -631,7 +591,7 @@ Within one scope, the `(file_pattern, root_to_strip)` pair must be unique across
|
|
|
631
591
|
>
|
|
632
592
|
> **Pass all rules at once.** Sequential single-rule decomposes rewrite `.multi_level.json` and only the last rule survives — bundle every rule for a given component into one override. Use [`sf decomposer verify`](#sf-decomposer-verify) to confirm a new config round-trips before committing it.
|
|
633
593
|
|
|
634
|
-
|
|
594
|
+
#### Opting in from the CLI
|
|
635
595
|
|
|
636
596
|
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`):
|
|
637
597
|
|
|
@@ -649,31 +609,67 @@ The post-retrieve hook automatically picks up `overrides` from `.sfdecomposer.co
|
|
|
649
609
|
|
|
650
610
|
---
|
|
651
611
|
|
|
652
|
-
|
|
612
|
+
### Ignore Files
|
|
653
613
|
|
|
654
|
-
|
|
614
|
+
#### .forceignore
|
|
655
615
|
|
|
656
616
|
The Salesforce CLI must **ignore** decomposed files and **allow** recomposed files. Use the [sample .forceignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.forceignore) and set patterns for the extensions you use (`.xml`, `.json`, `.yaml`, etc.).
|
|
657
617
|
|
|
658
|
-
|
|
618
|
+
#### .sfdecomposerignore
|
|
659
619
|
|
|
660
620
|
Optional. In the project root, list paths/patterns to skip when **decomposing** (same syntax as [.gitignore 2.22.1](https://git-scm.com/docs/gitignore)). Ignored files are not recomposed from.
|
|
661
621
|
|
|
662
|
-
|
|
622
|
+
#### .gitignore
|
|
663
623
|
|
|
664
|
-
Optional. Ignore recomposed metadata so it
|
|
624
|
+
Optional. Ignore recomposed metadata so it isn't committed. See the [sample .gitignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.gitignore).
|
|
665
625
|
|
|
666
626
|
---
|
|
667
627
|
|
|
668
|
-
|
|
628
|
+
### Troubleshooting
|
|
629
|
+
|
|
630
|
+
#### Missing sfdx-project.json
|
|
631
|
+
|
|
632
|
+
The plugin looks for `sfdx-project.json` from the current directory up to the drive root. If it's not found:
|
|
633
|
+
|
|
634
|
+
```
|
|
635
|
+
Error (1): sfdx-project.json not found in any parent directory.
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
#### Package Directories Not Found for Given Metadata Type
|
|
639
|
+
|
|
640
|
+
This plugin relies on the @salesforce/source-deploy-retrieve metadata registry to map each metadata type to its expected directory name.
|
|
641
|
+
|
|
642
|
+
If you provide a metadata type whose corresponding directory does not exist in any of your package directories, the plugin will fail with:
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
No directories named ${metadataTypeEntry.directoryName} were found in any package directory.
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
For example, if you attempt to decompose Custom Labels but none of your package directories contain a "labels" folder, the plugin will throw this error.
|
|
649
|
+
|
|
650
|
+
#### XML disassemble output (Rust crate)
|
|
651
|
+
|
|
652
|
+
The underlying Rust crate logs through [env_logger](https://docs.rs/env_logger). Set `RUST_LOG` to opt into more verbosity:
|
|
669
653
|
|
|
670
|
-
|
|
654
|
+
| Level | What it covers |
|
|
655
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
656
|
+
| `RUST_LOG=error` | Default. Parse errors and skipped files (leaf-only XML — primitives only, nothing to decompose). |
|
|
657
|
+
| `RUST_LOG=warn` | Adds [sibling-collision fallback](#filename-safety-unique-id) signals — one line per colliding group (parent tag, collided id, sibling count). **Recommended in CI** when shipping overrides. |
|
|
658
|
+
|
|
659
|
+
Example `WARN` (CustomApplication where four `actionOverrides` siblings shared the action name `View`):
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
[2026-05-04T15:21:09Z WARN config_disassembler::xml::builders::build_disassembled_files]
|
|
663
|
+
uniqueIdElements collision: <actionOverrides> id "View" matched 4 sibling elements;
|
|
664
|
+
falling back to SHA-256 content hashes for the colliding group.
|
|
665
|
+
Consider adding more discriminating fields to uniqueIdElements for this metadata type.
|
|
666
|
+
```
|
|
671
667
|
|
|
672
668
|
---
|
|
673
669
|
|
|
674
|
-
|
|
670
|
+
### Built With
|
|
675
671
|
|
|
676
|
-
- [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) – Disassemble XML (and other config formats) into smaller, manageable files and reassemble when needed. Node.js + Rust (NAPI-RS).
|
|
672
|
+
- [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) – Disassemble XML (and other config formats) into smaller, manageable files and reassemble when needed. Node.js + Rust (NAPI-RS).
|
|
677
673
|
- [@salesforce/source-deploy-retrieve](https://github.com/forcedotcom/source-deploy-retrieve) – JavaScript toolkit for working with Salesforce metadata.
|
|
678
674
|
|
|
679
675
|
---
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sf-decomposer",
|
|
3
3
|
"description": "Split large Salesforce metadata files into version-control-friendly pieces and rebuild deployment-ready files.",
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.25.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@oclif/core": "4.11.4",
|
|
7
7
|
"@salesforce/core": "8.26.3",
|
|
8
8
|
"@salesforce/sf-plugins-core": "12.2.6",
|
|
9
|
-
"@salesforce/source-deploy-retrieve": "12.
|
|
9
|
+
"@salesforce/source-deploy-retrieve": "12.36.0",
|
|
10
10
|
"config-disassembler": "2.3.0"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|