@salesforce/ui-bundle-template-base-sfdx-project 11.12.1 → 11.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.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Pure, importable XML helpers for org-setup.mjs network-metadata mutation
3
+ * (W-23043734 — Infrastructure Hygiene).
4
+ *
5
+ * These functions replace the script's former blind-regex editing of
6
+ * `*.network-meta.xml`. The contract is:
7
+ *
8
+ * 1. Parse the XML with `fast-xml-parser` ONLY to assert the expected nodes
9
+ * exist — this kills the old silent no-op (the regex matched nothing on a
10
+ * reformatted file yet the file was still rewritten + success logged).
11
+ * 2. Decide idempotency on the parsed model (profile already a member, self-reg
12
+ * already enabled) so callers keep their existing skip semantics + messages.
13
+ * 3. Mutate via a TARGETED string edit on the original text — NOT a full
14
+ * re-serialize. The XML declaration, the `xmlns` on <Network>,
15
+ * self-closing tags, and the file's existing indentation are preserved
16
+ * byte-for-byte, so a configured file re-run is a true no-op and there is
17
+ * zero deploy-diff hazard on this org-deployed file. (XMLBuilder/prettier
18
+ * are intentionally NOT used — a full re-serialize would churn formatting.)
19
+ * Profile names are asserted free of XML-special characters and interpolated
20
+ * raw — we reject such names rather than entity-escape them (a special
21
+ * character in a config-authored profile name signals a mistake).
22
+ *
23
+ * Pure (no fs / no process): org-setup.mjs reads/writes the file and owns the
24
+ * per-call-site fail mode (throw StepError vs. loud console.error); these
25
+ * helpers only parse, assert, and return the edited text. Kept in a dedicated
26
+ * module so they carry automated coverage: the shipped SFDX package has no test
27
+ * runner, so the specs live in the consuming ui-bundle-template-cli package
28
+ * (patches-cli/test/unit/org-setup-xml.spec.ts) rather than bundling into every
29
+ * generated app's dist/scripts.
30
+ */
31
+
32
+ import { XMLParser } from 'fast-xml-parser';
33
+
34
+ const NETWORK_ROOT = 'Network';
35
+
36
+ /** Parser tuned to read the document faithfully: keep attributes, never coerce
37
+ * values (so `<selfRegistration>false</...>` stays the string "false"). */
38
+ const parser = new XMLParser({
39
+ ignoreAttributes: false,
40
+ parseTagValue: false,
41
+ parseAttributeValue: false,
42
+ });
43
+
44
+ /**
45
+ * Thrown when a required node is absent from the network metadata. Callers map
46
+ * this to the fail mode appropriate to their orchestration position: the
47
+ * post-deploy selfReg step rethrows it as a recorded StepError; the best-effort
48
+ * pre-deploy prep logs it loudly without aborting.
49
+ */
50
+ export class NetworkXmlError extends Error {}
51
+
52
+ /** Characters that would need XML entity-escaping if interpolated into markup. */
53
+ const XML_SPECIAL_CHARS = /[&<>"']/;
54
+
55
+ /**
56
+ * Assert a profile name is safe to interpolate into the network metadata as-is.
57
+ *
58
+ * We deliberately do NOT entity-escape: a self-reg profile name is authored in
59
+ * `org-setup.config.json`, and one containing `&`, `<`, `>`, or quotes almost
60
+ * certainly signals a config mistake rather than a genuine name. Rather than
61
+ * silently rewrite it into `&amp;`-style entities, fail loudly so the author
62
+ * fixes the config. Throws NetworkXmlError (mapped by callers to their fail
63
+ * mode) when the name contains an XML-special character.
64
+ */
65
+ export function assertSafeProfileName(name) {
66
+ if (XML_SPECIAL_CHARS.test(String(name))) {
67
+ throw new NetworkXmlError(
68
+ `profile name "${name}" contains an XML-special character (& < > " '); ` +
69
+ `use a name without these characters in org-setup.config.json`,
70
+ );
71
+ }
72
+ return String(name);
73
+ }
74
+
75
+ /**
76
+ * Parse + assert this is a Network metadata document. Returns the parsed
77
+ * `Network` object. Throws NetworkXmlError if the root is missing (e.g. a file
78
+ * that is not actually network metadata).
79
+ */
80
+ export function parseNetwork(xml) {
81
+ let parsed;
82
+ try {
83
+ parsed = parser.parse(xml);
84
+ } catch (e) {
85
+ throw new NetworkXmlError(`network metadata is not well-formed XML: ${e.message}`);
86
+ }
87
+ const network = parsed?.[NETWORK_ROOT];
88
+ if (network == null || typeof network !== 'object') {
89
+ throw new NetworkXmlError(`expected a <${NETWORK_ROOT}> root node, but none was found`);
90
+ }
91
+ return network;
92
+ }
93
+
94
+ /** Normalize a fast-xml-parser child (absent | scalar | array) to an array. */
95
+ function asArray(node) {
96
+ if (node == null) return [];
97
+ return Array.isArray(node) ? node : [node];
98
+ }
99
+
100
+ /**
101
+ * Indentation for a new <networkMemberGroups> child, derived from the file's
102
+ * own formatting rather than a fixed width — so the targeted edit stays
103
+ * byte-faithful on reformatted (e.g. 2-space) files, not just the shipped
104
+ * 4-space template. Prefer an existing
105
+ * sibling <profile>'s exact indent; otherwise fall back to the
106
+ * <networkMemberGroups> indent doubled (it is always a direct child of the
107
+ * <Network> root, i.e. one level deep).
108
+ */
109
+ function memberGroupChildIndent(xml) {
110
+ const sibling = xml.match(/\n([ \t]+)<profile>/);
111
+ if (sibling) return sibling[1];
112
+ const parent = xml.match(/\n([ \t]*)<networkMemberGroups>/);
113
+ const parentIndent = parent ? parent[1] : ' ';
114
+ return parentIndent + (parentIndent || ' ');
115
+ }
116
+
117
+ /**
118
+ * Ensure `selfRegProfile` is listed under <networkMemberGroups>.
119
+ *
120
+ * Asserts <networkMemberGroups> exists (throws NetworkXmlError if not). Returns
121
+ * `{ xml, changed }`: `changed: false` (input returned unchanged) when the
122
+ * profile is already a member; otherwise the profile is inserted via a targeted
123
+ * edit right after the opening <networkMemberGroups> tag and `changed: true`.
124
+ */
125
+ export function addProfileToMemberGroups(xml, selfRegProfile) {
126
+ const network = parseNetwork(xml);
127
+ if (!('networkMemberGroups' in network) || network.networkMemberGroups == null) {
128
+ throw new NetworkXmlError(
129
+ 'network metadata has no <networkMemberGroups> node to add the self-reg profile to',
130
+ );
131
+ }
132
+ const existing = asArray(network.networkMemberGroups.profile).map((p) => String(p));
133
+ if (existing.includes(String(selfRegProfile))) {
134
+ return { xml, changed: false };
135
+ }
136
+ // Reject names with XML-special characters rather than escaping them (see
137
+ // assertSafeProfileName) before interpolating raw.
138
+ const safeName = assertSafeProfileName(selfRegProfile);
139
+ // Targeted edit: insert after the open tag, matching the file's own member
140
+ // indent (preserves formatting on reformatted files).
141
+ const indent = memberGroupChildIndent(xml);
142
+ const updated = xml.replace(
143
+ /(<networkMemberGroups>)/,
144
+ `$1\n${indent}<profile>${safeName}</profile>`,
145
+ );
146
+ return { xml: updated, changed: true };
147
+ }
148
+
149
+ /**
150
+ * Enable self-registration: set <selfRegistration>true</selfRegistration> and
151
+ * add <selfRegProfile>.
152
+ *
153
+ * Asserts <selfRegistration> exists (throws NetworkXmlError if not). Returns
154
+ * `{ xml, changed }`: `changed: false` (input returned unchanged) when self-reg
155
+ * is already enabled or a selfRegProfile is already present — same skip
156
+ * semantics as before; otherwise applies a targeted edit and `changed: true`.
157
+ */
158
+ export function enableSelfRegInXml(xml, selfRegProfile) {
159
+ const network = parseNetwork(xml);
160
+ if (!('selfRegistration' in network)) {
161
+ throw new NetworkXmlError(
162
+ 'network metadata has no <selfRegistration> node to enable self-registration on',
163
+ );
164
+ }
165
+ const alreadyEnabled = String(network.selfRegistration) === 'true';
166
+ const alreadyHasProfile = 'selfRegProfile' in network;
167
+ if (alreadyEnabled || alreadyHasProfile) {
168
+ return { xml, changed: false };
169
+ }
170
+ // Reject names with XML-special characters rather than escaping them (see
171
+ // assertSafeProfileName) before interpolating raw.
172
+ const safeName = assertSafeProfileName(selfRegProfile);
173
+ let updated = xml.replace(
174
+ /<selfRegistration>false<\/selfRegistration>/,
175
+ '<selfRegistration>true</selfRegistration>',
176
+ );
177
+ updated = updated.replace(
178
+ /(\s*)(<selfRegistration>)/,
179
+ `$1<selfRegProfile>${safeName}</selfRegProfile>\n$1$2`,
180
+ );
181
+ return { xml: updated, changed: true };
182
+ }