sf-decomposer 6.11.0 → 6.13.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 CHANGED
@@ -5,6 +5,22 @@
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.13.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.12.0...v6.13.0) (2026-04-30)
9
+
10
+
11
+ ### Features
12
+
13
+ * **decompose:** add per-component overrides ([#415](https://github.com/mcarvin8/sf-decomposer/issues/415)) ([47d7c79](https://github.com/mcarvin8/sf-decomposer/commit/47d7c794db17ccc59e832309a0a4980d736ab89c))
14
+ * **deps:** bump dependabot/fetch-metadata from 2 to 3 ([#412](https://github.com/mcarvin8/sf-decomposer/issues/412)) ([6b0a24b](https://github.com/mcarvin8/sf-decomposer/commit/6b0a24b9c83dfb6fb9ef1d8dc8e2dfbec23200bf))
15
+ * **metadata:** bump @salesforce/source-deploy-retrieve ([#413](https://github.com/mcarvin8/sf-decomposer/issues/413)) ([e261049](https://github.com/mcarvin8/sf-decomposer/commit/e261049cc07a94642c1e80cf29dbd1e96a7183bd))
16
+
17
+ ## [6.12.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.11.0...v6.12.0) (2026-04-27)
18
+
19
+
20
+ ### Features
21
+
22
+ * add per-metadata-type overrides for decompose ([#407](https://github.com/mcarvin8/sf-decomposer/issues/407)) ([43be31d](https://github.com/mcarvin8/sf-decomposer/commit/43be31d6190e095a0dbc5b763e2439582c5b4214))
23
+
8
24
  ## [6.11.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.10.0...v6.11.0) (2026-04-24)
9
25
 
10
26
 
package/README.md CHANGED
@@ -26,6 +26,7 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
26
26
  - [Exceptions](#exceptions)
27
27
  - [Troubleshooting](#troubleshooting)
28
28
  - [Hooks](#hooks)
29
+ - [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
29
30
  - [Ignore Files](#ignore-files)
30
31
  - [.forceignore](#forceignore)
31
32
  - [.sfdecomposerignore](#sfdecomposerignore)
@@ -78,7 +79,7 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
78
79
 
79
80
  ## Requirements
80
81
 
81
- The [xml-disassembler](https://github.com/mcarvin8/xml-disassembler) NodeJS dependency, which depends on a Rust crate, ships with native binaries for these platforms:
82
+ The [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) package, which depends on a Rust crate, ships with native binaries for these platforms:
82
83
 
83
84
  | Platform | Architectures |
84
85
  | ----------- | ---------------------------------- |
@@ -86,7 +87,7 @@ The [xml-disassembler](https://github.com/mcarvin8/xml-disassembler) NodeJS depe
86
87
  | **Linux** | x64, arm64, ia32 |
87
88
  | **Windows** | x64 |
88
89
 
89
- If other platforms or architectures require support, please open an issue in the [Rust repository](https://github.com/mcarvin8/xml-disassembler-rust).
90
+ If other platforms or architectures require support, please open an issue in [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node/issues).
90
91
 
91
92
  ---
92
93
 
@@ -123,7 +124,7 @@ Decomposes metadata in all local package directories (from `sfdx-project.json`)
123
124
 
124
125
  ```
125
126
  USAGE
126
- $ sf decomposer decompose [-m <value>] [-x <value>] [-f <value>] [-i <value>] [-s <value>] [--prepurge --postpurge -p --json]
127
+ $ sf decomposer decompose [-m <value>] [-x <value>] [-f <value>] [-i <value>] [-s <value>] [--prepurge --postpurge -p -c --json]
127
128
 
128
129
  FLAGS
129
130
  -m, --metadata-type=<value> Metadata suffix to process (e.g. flow, labels). Repeatable. Optional when --manifest is provided.
@@ -134,6 +135,7 @@ FLAGS
134
135
  --prepurge Remove existing decomposed files before decomposing [default: false]
135
136
  --postpurge Remove original metadata files after decomposing [default: false]
136
137
  -p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions
138
+ -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. See Per-Type & Per-Component Overrides. [default: false]
137
139
 
138
140
  GLOBAL FLAGS
139
141
  --json Output as JSON.
@@ -230,7 +232,7 @@ sf project deploy start -x "manifest/package.xml"
230
232
 
231
233
  ## Decompose Strategies
232
234
 
233
- > **Important:** Use one strategy per metadata type. To switch from `unique-id` to `grouped-by-tag`, run decompose with `--prepurge` and `-s "grouped-by-tag"` to regenerate.
235
+ > **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.
234
236
 
235
237
  - **unique-id** (default): Each nested element goes to its own file, named by unique-id fields or content hash. Leaf elements stay in a file named like the original XML.
236
238
  - **grouped-by-tag**: All elements with the same tag (e.g. `<fieldPermissions>`) go into one file named after the tag (e.g. `fieldPermissions.xml`). Leaf elements are still grouped in the original-named file.
@@ -332,14 +334,14 @@ For example, if you attempt to decompose Custom Labels but none of your package
332
334
 
333
335
  ### XML disassemble output (Rust crate)
334
336
 
335
- The xml-disassembler Node plugin uses a **Rust crate** for XML decomposing and recomposing. Disassemble errors and messages are shown in the terminal.
337
+ The config-disassembler Node plugin uses a **Rust crate** for XML decomposing and recomposing. Disassemble errors and messages are shown in the terminal.
336
338
 
337
339
  Control verbosity with the `RUST_LOG` environment variable (e.g. `RUST_LOG=debug` for detailed output).
338
340
 
339
341
  Example output in the terminal (Rust log format):
340
342
 
341
343
  ```
342
- [2026-02-11T22:52:32Z ERROR xml_disassembler::builders::build_disassembled_files] The XML file C:\Users\matthew.carvin\Documents\sf-decomposer\fixtures\package-dir-1\permissionsets\only_leafs.permissionset-meta.xml only has leaf elements. This file will not be disassembled.
344
+ [2026-04-30T12:34:38Z ERROR config_disassembler::xml::builders::build_disassembled_files] The XML file C:\Users\matthew.carvin\Documents\sf-decomposer\fixtures\package-dir-1\permissionsets\only_leafs.permissionset-meta.xml only has leaf elements. This file will not be disassembled.
343
345
  ```
344
346
 
345
347
  ### Files with only leaf elements
@@ -357,18 +359,101 @@ Put **.sfdecomposer.config.json** in the project root to run:
357
359
  - **After** `sf project retrieve start`: decompose.
358
360
  - **Before** `sf project deploy start` / `sf project deploy validate`: recompose.
359
361
 
360
- Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json).
362
+ Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample config with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy/etc. by metadata type or by individual component.
361
363
 
362
- | Option | Required | Description |
363
- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
364
- | `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. |
365
- | `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. |
366
- | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
367
- | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
368
- | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
369
- | `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
370
- | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
371
- | `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
364
+ | Option | Required | Description |
365
+ | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
366
+ | `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. |
367
+ | `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. |
368
+ | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
369
+ | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
370
+ | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
371
+ | `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
372
+ | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
373
+ | `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
374
+ | `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). |
375
+
376
+ ---
377
+
378
+ ## Per-Type & Per-Component Overrides
379
+
380
+ 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.
381
+
382
+ By default, a single decompose run uses one format and one strategy across every metadata type. The optional `overrides` array in `.sfdecomposer.config.json` lets you vary a small set of options per metadata suffix (**type-scope**) or per individual SDR component (**component-scope**) without splitting the run into multiple invocations.
383
+
384
+ ```json
385
+ {
386
+ "metadataSuffixes": "labels,workflow,profile,flow,permissionset",
387
+ "ignorePackageDirectories": "force-app,examples",
388
+ "prePurge": true,
389
+ "postPurge": true,
390
+ "decomposedFormat": "xml",
391
+ "strategy": "unique-id",
392
+ "overrides": [
393
+ { "metadataTypes": ["flow"], "decomposedFormat": "yaml" },
394
+ {
395
+ "metadataTypes": ["permissionset", "mutingpermissionset"],
396
+ "strategy": "grouped-by-tag",
397
+ "decomposeNestedPermissions": true
398
+ },
399
+ {
400
+ "components": ["permissionset:HR_Admin", "permissionset:Big_PermSet"],
401
+ "strategy": "grouped-by-tag",
402
+ "decomposeNestedPermissions": true
403
+ }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ ### What can be overridden
409
+
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
+ | `prePurge` | Per-scope prePurge (decompose). Component-scope `prePurge` only purges the named component's decomposed directory. |
418
+ | `postPurge` | Per-scope postPurge (decompose: remove originals after decomposing). |
419
+
420
+ Run-scope options (`metadataSuffixes`, `manifest`, `ignorePackageDirectories`) are **not** valid inside an override; the plugin will throw if they are present.
421
+
422
+ #### Component key conventions
423
+
424
+ The `<fullName>` part of a component key is the SDR fullName for the component, matching the basename of the decomposed directory:
425
+
426
+ - **Plain types** (e.g. `permissionset`, `flow`, `profile`, `workflow`): use the file stem, e.g. `permissionset:HR_Admin` for `permissionsets/HR_Admin.permissionset-meta.xml`.
427
+ - **Strict-directory types** (e.g. `bot`): use the bot directory name, e.g. `bot:My_Bot` for `bots/My_Bot/My_Bot.bot-meta.xml`.
428
+ - **Folder-typed metadata** (e.g. `report`, `dashboard`, `email`, `document`): the unit of decomposition is the folder; use the folder name, e.g. `report:MyFolder` to scope every report inside `reports/MyFolder/`.
429
+ - **`labels`**: there is exactly one labels file per labels directory, so component-scope keys are not meaningful — use the type-scope `metadataTypes: ["labels"]` instead.
430
+
431
+ 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.
432
+
433
+ ### Precedence
434
+
435
+ For each component, each option is resolved independently in this order (highest first):
436
+
437
+ 1. The component-scope override value (matching `<suffix>:<fullName>` in `components`), if set.
438
+ 2. The type-scope override value (matching `<suffix>` in `metadataTypes`), if set.
439
+ 3. The run-wide value (CLI flag, hook config top-level field, or built-in default).
440
+ 4. Hard plugin rules (e.g. `labels` and `loyaltyProgramSetup` forced to `unique-id`) override all of the above.
441
+
442
+ ### Opting in from the CLI
443
+
444
+ 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`):
445
+
446
+ ```bash
447
+ sf decomposer decompose -m "flow" -m "permissionset" -c
448
+ ```
449
+
450
+ When `--config` is set, **only** the `overrides` array is consumed from the file. Top-level fields like `decomposedFormat`, `strategy`, `metadataSuffixes`, etc. are ignored — the CLI flags remain the source of truth for run-wide values. This keeps direct CLI behavior predictable and lets you reuse the same config file as the post-retrieve hook without any surprises.
451
+
452
+ If `--config` is set but `.sfdecomposer.config.json` is missing from the repo root, the command fails with a clear error.
453
+
454
+ `recompose` does not accept `--config` because it does not need the override information — format is auto-detected from the decomposed files on disk and recompose does not depend on strategy.
455
+
456
+ The post-retrieve hook automatically picks up `overrides` from `.sfdecomposer.config.json` — no extra setup required. Existing config files without an `overrides` field continue to behave exactly as before.
372
457
 
373
458
  ---
374
459
 
@@ -396,8 +481,7 @@ Bugs and feature requests: [open an issue](https://github.com/mcarvin8/sf-decomp
396
481
 
397
482
  ## Built With
398
483
 
399
- - [xml-disassembler](https://github.com/mcarvin8/xml-disassembler) – Disassemble XML into smaller, manageable files and reassemble when needed. Node.js + Rust (Neon). Includes prebuilt binaries for macOS (x64, arm64), Linux (x64, arm64, ia32), and Windows (x64).
400
- - [fs-extra](https://github.com/jprichardson/node-fs-extra) – Node.js: extra methods for the `fs` object like copy(), remove(), mkdirs().
484
+ - [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 (Neon). See [Requirements](#requirements).
401
485
  - [@salesforce/source-deploy-retrieve](https://github.com/forcedotcom/source-deploy-retrieve) – JavaScript toolkit for working with Salesforce metadata.
402
486
 
403
487
  ---
@@ -408,4 +492,4 @@ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/mcarvin8/sf-
408
492
 
409
493
  ## License
410
494
 
411
- MIT. See [LICENSE](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/LICENSE.md).
495
+ [MIT](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/LICENSE.md)
@@ -13,6 +13,7 @@ export default class DecomposerDecompose extends SfCommand<DecomposerResult> {
13
13
  'ignore-package-directory': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  strategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
15
  'decompose-nested-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ config: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
17
  };
17
18
  run(): Promise<DecomposerResult>;
18
19
  }
@@ -3,6 +3,7 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
3
3
  import { Messages } from '@salesforce/core';
4
4
  import { DECOMPOSED_FILE_TYPES, DECOMPOSED_STRATEGIES } from '../../helpers/constants.js';
5
5
  import { decomposeMetadataTypes } from '../../core/decomposeMetadataTypes.js';
6
+ import { loadOverridesFromConfig, resolveDefaultConfigPath } from '../../helpers/configOverrides.js';
6
7
  Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
7
8
  const messages = Messages.loadMessages('sf-decomposer', 'decomposer.decompose');
8
9
  export default class DecomposerDecompose extends SfCommand {
@@ -60,12 +61,19 @@ export default class DecomposerDecompose extends SfCommand {
60
61
  required: false,
61
62
  default: false,
62
63
  }),
64
+ config: Flags.boolean({
65
+ summary: messages.getMessage('flags.config.summary'),
66
+ char: 'c',
67
+ required: false,
68
+ default: false,
69
+ }),
63
70
  };
64
71
  async run() {
65
72
  const { flags } = await this.parse(DecomposerDecompose);
66
73
  if (!flags['metadata-type'] && !flags['manifest']) {
67
74
  throw messages.createError('error.missingMetadataOrManifest');
68
75
  }
76
+ const overrides = flags['config'] ? await loadOverridesFromConfig(await resolveDefaultConfigPath()) : undefined;
69
77
  return decomposeMetadataTypes({
70
78
  metadataTypes: flags['metadata-type'],
71
79
  prepurge: flags['prepurge'],
@@ -75,6 +83,7 @@ export default class DecomposerDecompose extends SfCommand {
75
83
  strategy: flags['strategy'],
76
84
  decomposeNestedPerms: flags['decompose-nested-permissions'],
77
85
  manifest: flags['manifest'],
86
+ overrides,
78
87
  log: this.log.bind(this),
79
88
  });
80
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"decompose.js","sourceRoot":"","sources":["../../../src/commands/decomposer/decompose.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"decompose.js","sourceRoot":"","sources":["../../../src/commands/decomposer/decompose.ts"],"names":[],"mappings":"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"}
@@ -4,8 +4,9 @@ import { getRegistryValuesBySuffix } from '../metadata/getRegistryValuesBySuffix
4
4
  import { parseManifest } from '../metadata/parseManifest.js';
5
5
  import { decomposeFileHandler } from '../service/decompose/decomposeFileHandler.js';
6
6
  import { CONCURRENCY_LIMITS } from '../helpers/constants.js';
7
+ import { resolveDecomposeOptionsForType } from '../helpers/configOverrides.js';
7
8
  export async function decomposeMetadataTypes(options) {
8
- const { metadataTypes, prepurge, postpurge, format, ignoreDirs, strategy, decomposeNestedPerms, manifest, log } = options;
9
+ const { metadataTypes, prepurge, postpurge, format, ignoreDirs, strategy, decomposeNestedPerms, manifest, overrides, log, } = options;
9
10
  let manifestFilter;
10
11
  let effectiveTypes;
11
12
  if (manifest) {
@@ -47,14 +48,12 @@ export async function decomposeMetadataTypes(options) {
47
48
  log(`Skipping ${metadataType}: ${message}`);
48
49
  return;
49
50
  }
50
- let effectiveStrategy = strategy;
51
- if (metadataType === 'labels' && strategy === 'grouped-by-tag') {
52
- effectiveStrategy = 'unique-id';
53
- }
54
- if (metadataType === 'loyaltyProgramSetup' && strategy === 'grouped-by-tag') {
55
- effectiveStrategy = 'unique-id';
56
- }
57
- await decomposeFileHandler(metaAttributes, prepurge, postpurge, format, ignorePath, effectiveStrategy, decomposeNestedPerms, manifestXmlPaths);
51
+ // Type-scope resolved options serve as the base for component-scope resolution further
52
+ // down the call stack. Hard strategy rules (labels / loyaltyProgramSetup) are applied per
53
+ // file inside the disassembler so they remain in force even when a component-scope override
54
+ // tries to flip the strategy.
55
+ const typeResolved = resolveDecomposeOptionsForType(metadataType, { format, strategy, decomposeNestedPerms, prepurge, postpurge }, overrides);
56
+ await decomposeFileHandler(metaAttributes, typeResolved, ignorePath, overrides, manifestXmlPaths);
58
57
  processed.push(metadataType);
59
58
  log(`All metadata files have been decomposed for the metadata type: ${metadataType}`);
60
59
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"decomposeMetadataTypes.js","sourceRoot":"","sources":["../../src/core/decomposeMetadataTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"decomposeMetadataTypes.js","sourceRoot":"","sources":["../../src/core/decomposeMetadataTypes.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,aAAa,EAAkB,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAE/E,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAyB;IACpE,MAAM,EACJ,aAAa,EACb,QAAQ,EACR,SAAS,EACT,MAAM,EACN,UAAU,EACV,QAAQ,EACR,oBAAoB,EACpB,QAAQ,EACR,SAAS,EACT,GAAG,GACJ,GAAG,OAAO,CAAC;IAEZ,IAAI,cAA0C,CAAC;IAC/C,IAAI,cAAwB,CAAC;IAE7B,IAAI,QAAQ,EAAE,CAAC;QACb,cAAc,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC3D,IAAI,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YACvD,cAAc,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3E,CAAC;aAAM,CAAC;YACN,cAAc,GAAG,cAAc,CAAC,QAAQ,CAAC;QAC3C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjD,MAAM,KAAK,CAAC,wDAAwD,CAAC,CAAC;QACxE,CAAC;QACD,cAAc,GAAG,aAAa,CAAC;IACjC,CAAC;IAED,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,GAAG,CAAC,oEAAoE,CAAC,CAAC;QAC1E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC1B,CAAC;IAED,4EAA4E;IAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAExD,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAChD,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,MAAM,gBAAgB,GAAG,cAAc,EAAE,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAE9E,IAAI,cAAc,CAAC;QACnB,IAAI,UAAkB,CAAC;QACvB,IAAI,CAAC;YACH,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,GAAG,MAAM,yBAAyB,CAAC,YAAY,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;QAC5G,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,uGAAuG;YACvG,IAAI,CAAC,cAAc;gBAAE,MAAM,GAAG,CAAC;YAC/B,gGAAgG;YAChG,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,GAAG,CAAC,YAAY,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,uFAAuF;QACvF,0FAA0F;QAC1F,4FAA4F;QAC5F,8BAA8B;QAC9B,MAAM,YAAY,GAAG,8BAA8B,CACjD,YAAY,EACZ,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,EAAE,SAAS,EAAE,EAC/D,SAAS,CACV,CAAC;QAEF,MAAM,oBAAoB,CAAC,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;QAElG,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7B,GAAG,CAAC,kEAAkE,YAAY,EAAE,CAAC,CAAC;IACxF,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { DecomposerOverride } from './types.js';
2
+ /**
3
+ * Resolve the absolute path of the default `.sfdecomposer.config.json`, located in the
4
+ * repo root (the nearest ancestor directory that contains `sfdx-project.json`). Throws
5
+ * a clear error when the repo root cannot be located or the config file does not exist.
6
+ */
7
+ export declare function resolveDefaultConfigPath(): Promise<string>;
8
+ export type ResolvedDecomposeTypeOptions = {
9
+ format: string;
10
+ strategy: string;
11
+ decomposeNestedPerms: boolean;
12
+ prepurge: boolean;
13
+ postpurge: boolean;
14
+ };
15
+ /**
16
+ * Load and validate the `overrides` array from a `.sfdecomposer.config.json` file.
17
+ * Returns an empty array if the file is missing, unreadable, or contains no overrides.
18
+ */
19
+ export declare function loadOverridesFromConfig(configPath: string): Promise<DecomposerOverride[]>;
20
+ /**
21
+ * Validate that the overrides array is well-formed. Throws on any structural problem.
22
+ * Unknown override keys are tolerated (ignored), but forbidden run-scope keys throw.
23
+ */
24
+ export declare function validateOverrides(overrides: DecomposerOverride[]): void;
25
+ /**
26
+ * Parse a component override key of the form `<metadataSuffix>:<fullName>`. Returns `undefined`
27
+ * when the key is malformed. Only the first colon is treated as the delimiter so fullNames that
28
+ * contain `/` (folder-typed metadata such as `report:MyFolder/MyReport`) are preserved verbatim.
29
+ */
30
+ export declare function parseComponentKey(key: string): {
31
+ metadataType: string;
32
+ fullName: string;
33
+ } | undefined;
34
+ /**
35
+ * Find the override (if any) that targets a specific metadata suffix.
36
+ */
37
+ export declare function getOverrideForType(metadataType: string, overrides?: DecomposerOverride[]): DecomposerOverride | undefined;
38
+ /**
39
+ * Find the override (if any) that targets a specific component, identified by its metadata
40
+ * suffix and SDR fullName.
41
+ */
42
+ export declare function getOverrideForComponent(metadataType: string, fullName: string, overrides?: DecomposerOverride[]): DecomposerOverride | undefined;
43
+ /**
44
+ * Returns true when at least one override targets a component of the given metadata type.
45
+ * Used by the decompose handler to decide whether per-component enumeration is required.
46
+ */
47
+ export declare function hasComponentOverridesForType(metadataType: string, overrides?: DecomposerOverride[]): boolean;
48
+ /**
49
+ * Resolve the effective decompose options for a single metadata type. The base values are
50
+ * the run-wide options (CLI flags or defaults); per-type override values, when present, win.
51
+ */
52
+ export declare function resolveDecomposeOptionsForType(metadataType: string, base: ResolvedDecomposeTypeOptions, overrides?: DecomposerOverride[]): ResolvedDecomposeTypeOptions;
53
+ /**
54
+ * Resolve the effective decompose options for a single component. Precedence (highest first):
55
+ * component-scoped override fields (via `components`), then type-scoped resolved values
56
+ * (already applied by `resolveDecomposeOptionsForType`), then run-wide base values (passed
57
+ * through as the typeResolved fallback).
58
+ *
59
+ * Hard plugin rules (e.g. labels and loyaltyProgramSetup forced to `unique-id`) are applied
60
+ * separately by callers, not here, so this function stays pure.
61
+ */
62
+ export declare function resolveDecomposeOptionsForComponent(metadataType: string, fullName: string, typeResolved: ResolvedDecomposeTypeOptions, overrides?: DecomposerOverride[]): ResolvedDecomposeTypeOptions;
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { getRepoRoot } from '../service/core/getRepoRoot.js';
5
+ import { DECOMPOSED_FILE_TYPES, DECOMPOSED_STRATEGIES, HOOK_CONFIG_JSON } from './constants.js';
6
+ /**
7
+ * Resolve the absolute path of the default `.sfdecomposer.config.json`, located in the
8
+ * repo root (the nearest ancestor directory that contains `sfdx-project.json`). Throws
9
+ * a clear error when the repo root cannot be located or the config file does not exist.
10
+ */
11
+ export async function resolveDefaultConfigPath() {
12
+ const { repoRoot } = await getRepoRoot();
13
+ /* istanbul ignore if -- @preserve: getRepoRoot throws when no sfdx-project.json ancestor exists, so repoRoot is always defined here. */
14
+ if (!repoRoot) {
15
+ throw new Error(`Cannot locate ${HOOK_CONFIG_JSON}: repo root not found.`);
16
+ }
17
+ const configPath = resolve(repoRoot, HOOK_CONFIG_JSON);
18
+ try {
19
+ await access(configPath);
20
+ }
21
+ catch {
22
+ throw new Error(`--config was provided but ${HOOK_CONFIG_JSON} was not found at ${configPath}. ` +
23
+ 'Create the file in the repo root or omit --config.');
24
+ }
25
+ return configPath;
26
+ }
27
+ // Run-scope keys that must never appear inside an override entry. Any other key is treated
28
+ // as either a recognized override field (validated below) or as a forward-compatible unknown
29
+ // key (silently ignored).
30
+ const FORBIDDEN_OVERRIDE_KEYS = new Set(['manifest', 'metadataSuffixes', 'ignorePackageDirectories']);
31
+ /**
32
+ * Load and validate the `overrides` array from a `.sfdecomposer.config.json` file.
33
+ * Returns an empty array if the file is missing, unreadable, or contains no overrides.
34
+ */
35
+ export async function loadOverridesFromConfig(configPath) {
36
+ let raw;
37
+ try {
38
+ raw = await readFile(configPath, 'utf-8');
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ }
47
+ catch (err) {
48
+ /* istanbul ignore next -- @preserve: JSON.parse always throws SyntaxError instances. */
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ throw new Error(`Failed to parse ${configPath}: ${message}`);
51
+ }
52
+ const overrides = parsed.overrides;
53
+ if (!overrides)
54
+ return [];
55
+ if (!Array.isArray(overrides)) {
56
+ throw new Error(`"overrides" in ${configPath} must be an array.`);
57
+ }
58
+ validateOverrides(overrides);
59
+ return overrides;
60
+ }
61
+ /**
62
+ * Validate that the overrides array is well-formed. Throws on any structural problem.
63
+ * Unknown override keys are tolerated (ignored), but forbidden run-scope keys throw.
64
+ */
65
+ export function validateOverrides(overrides) {
66
+ const seenTypes = new Set();
67
+ const seenComponents = new Set();
68
+ for (let i = 0; i < overrides.length; i++) {
69
+ validateOverride(overrides[i], i, seenTypes, seenComponents);
70
+ }
71
+ }
72
+ function validateOverride(override, i, seenTypes, seenComponents) {
73
+ if (!override || typeof override !== 'object') {
74
+ throw new Error(`Override at index ${i} must be an object.`);
75
+ }
76
+ if (override.metadataTypes !== undefined && !Array.isArray(override.metadataTypes)) {
77
+ throw new Error(`Override at index ${i} has a non-array "metadataTypes".`);
78
+ }
79
+ if (override.components !== undefined && !Array.isArray(override.components)) {
80
+ throw new Error(`Override at index ${i} has a non-array "components".`);
81
+ }
82
+ const hasMetadataTypes = Array.isArray(override.metadataTypes) && override.metadataTypes.length > 0;
83
+ const hasComponents = Array.isArray(override.components) && override.components.length > 0;
84
+ if (!hasMetadataTypes && !hasComponents) {
85
+ throw new Error(`Override at index ${i} must include a non-empty "metadataTypes" or "components" array.`);
86
+ }
87
+ validateForbiddenKeys(override, i);
88
+ validateOverrideValues(override, i);
89
+ if (hasMetadataTypes) {
90
+ validateMetadataTypeEntries(override.metadataTypes, i, seenTypes);
91
+ }
92
+ if (hasComponents) {
93
+ validateComponentEntries(override.components, i, seenComponents);
94
+ }
95
+ }
96
+ function validateForbiddenKeys(override, i) {
97
+ for (const key of Object.keys(override)) {
98
+ if (FORBIDDEN_OVERRIDE_KEYS.has(key)) {
99
+ throw new Error(`Override at index ${i} contains "${key}", which is a run-scope option and cannot be set per metadata type.`);
100
+ }
101
+ }
102
+ }
103
+ function validateOverrideValues(override, i) {
104
+ if (override.decomposedFormat !== undefined && !DECOMPOSED_FILE_TYPES.includes(override.decomposedFormat)) {
105
+ throw new Error(`Override at index ${i} has invalid "decomposedFormat": "${override.decomposedFormat}". ` +
106
+ `Allowed values: ${DECOMPOSED_FILE_TYPES.join(', ')}.`);
107
+ }
108
+ if (override.strategy !== undefined && !DECOMPOSED_STRATEGIES.includes(override.strategy)) {
109
+ throw new Error(`Override at index ${i} has invalid "strategy": "${override.strategy}". ` +
110
+ `Allowed values: ${DECOMPOSED_STRATEGIES.join(', ')}.`);
111
+ }
112
+ }
113
+ function validateMetadataTypeEntries(metadataTypes, i, seenTypes) {
114
+ for (const metadataType of metadataTypes) {
115
+ if (typeof metadataType !== 'string' || metadataType.trim() === '') {
116
+ throw new Error(`Override at index ${i} contains an empty or non-string metadata type.`);
117
+ }
118
+ if (seenTypes.has(metadataType)) {
119
+ throw new Error(`Metadata type "${metadataType}" appears in more than one override. Each type may appear at most once.`);
120
+ }
121
+ seenTypes.add(metadataType);
122
+ }
123
+ }
124
+ function validateComponentEntries(components, i, seenComponents) {
125
+ for (const component of components) {
126
+ if (typeof component !== 'string' || component.trim() === '') {
127
+ throw new Error(`Override at index ${i} contains an empty or non-string component.`);
128
+ }
129
+ if (!parseComponentKey(component)) {
130
+ throw new Error(`Override at index ${i} has invalid component key "${component}". ` +
131
+ 'Expected format: "<metadataSuffix>:<fullName>" (e.g. "permissionset:HR_Admin").');
132
+ }
133
+ if (seenComponents.has(component)) {
134
+ throw new Error(`Component "${component}" appears in more than one override. Each component may appear at most once.`);
135
+ }
136
+ seenComponents.add(component);
137
+ }
138
+ }
139
+ /**
140
+ * Parse a component override key of the form `<metadataSuffix>:<fullName>`. Returns `undefined`
141
+ * when the key is malformed. Only the first colon is treated as the delimiter so fullNames that
142
+ * contain `/` (folder-typed metadata such as `report:MyFolder/MyReport`) are preserved verbatim.
143
+ */
144
+ export function parseComponentKey(key) {
145
+ const colonIdx = key.indexOf(':');
146
+ if (colonIdx <= 0 || colonIdx === key.length - 1)
147
+ return undefined;
148
+ const metadataType = key.slice(0, colonIdx).trim();
149
+ const fullName = key.slice(colonIdx + 1).trim();
150
+ if (!metadataType || !fullName)
151
+ return undefined;
152
+ return { metadataType, fullName };
153
+ }
154
+ /**
155
+ * Find the override (if any) that targets a specific metadata suffix.
156
+ */
157
+ export function getOverrideForType(metadataType, overrides) {
158
+ if (!overrides || overrides.length === 0)
159
+ return undefined;
160
+ return overrides.find((override) => override.metadataTypes?.includes(metadataType));
161
+ }
162
+ /**
163
+ * Find the override (if any) that targets a specific component, identified by its metadata
164
+ * suffix and SDR fullName.
165
+ */
166
+ export function getOverrideForComponent(metadataType, fullName, overrides) {
167
+ if (!overrides || overrides.length === 0)
168
+ return undefined;
169
+ const key = `${metadataType}:${fullName}`;
170
+ return overrides.find((override) => override.components?.includes(key));
171
+ }
172
+ /**
173
+ * Returns true when at least one override targets a component of the given metadata type.
174
+ * Used by the decompose handler to decide whether per-component enumeration is required.
175
+ */
176
+ export function hasComponentOverridesForType(metadataType, overrides) {
177
+ if (!overrides || overrides.length === 0)
178
+ return false;
179
+ const prefix = `${metadataType}:`;
180
+ return overrides.some((override) => override.components?.some((component) => component.startsWith(prefix)));
181
+ }
182
+ /**
183
+ * Resolve the effective decompose options for a single metadata type. The base values are
184
+ * the run-wide options (CLI flags or defaults); per-type override values, when present, win.
185
+ */
186
+ export function resolveDecomposeOptionsForType(metadataType, base, overrides) {
187
+ const override = getOverrideForType(metadataType, overrides);
188
+ if (!override)
189
+ return base;
190
+ return {
191
+ format: override.decomposedFormat ?? base.format,
192
+ strategy: override.strategy ?? base.strategy,
193
+ decomposeNestedPerms: override.decomposeNestedPermissions ?? base.decomposeNestedPerms,
194
+ prepurge: override.prePurge ?? base.prepurge,
195
+ postpurge: override.postPurge ?? base.postpurge,
196
+ };
197
+ }
198
+ /**
199
+ * Resolve the effective decompose options for a single component. Precedence (highest first):
200
+ * component-scoped override fields (via `components`), then type-scoped resolved values
201
+ * (already applied by `resolveDecomposeOptionsForType`), then run-wide base values (passed
202
+ * through as the typeResolved fallback).
203
+ *
204
+ * Hard plugin rules (e.g. labels and loyaltyProgramSetup forced to `unique-id`) are applied
205
+ * separately by callers, not here, so this function stays pure.
206
+ */
207
+ export function resolveDecomposeOptionsForComponent(metadataType, fullName, typeResolved, overrides) {
208
+ const componentOverride = getOverrideForComponent(metadataType, fullName, overrides);
209
+ if (!componentOverride)
210
+ return typeResolved;
211
+ return {
212
+ format: componentOverride.decomposedFormat ?? typeResolved.format,
213
+ strategy: componentOverride.strategy ?? typeResolved.strategy,
214
+ decomposeNestedPerms: componentOverride.decomposeNestedPermissions ?? typeResolved.decomposeNestedPerms,
215
+ prepurge: componentOverride.prePurge ?? typeResolved.prepurge,
216
+ postpurge: componentOverride.postPurge ?? typeResolved.postpurge,
217
+ };
218
+ }
219
+ //# sourceMappingURL=configOverrides.js.map