@salesforce/ui-bundle-template-feature-react-search 11.12.0 → 11.12.2

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/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [11.12.2](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.12.1...v11.12.2) (2026-07-09)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **template:** harden org-setup.mjs XML editing + temp-file/lock hygiene (W-23043734) ([#704](https://github.com/salesforce-experience-platform-emu/webapps/issues/704)) ([9dc682e](https://github.com/salesforce-experience-platform-emu/webapps/commit/9dc682efe168c1758a40d677b55d8da83add653e))
12
+
13
+
14
+
15
+ ## [11.12.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.12.0...v11.12.1) (2026-07-08)
16
+
17
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
18
+
19
+
20
+
21
+
22
+
6
23
  ## [11.12.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.11.1...v11.12.0) (2026-07-08)
7
24
 
8
25
  **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
@@ -18,8 +18,8 @@
18
18
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@salesforce/platform-sdk": "^11.12.0",
22
- "@salesforce/ui-bundle": "^11.12.0",
21
+ "@salesforce/platform-sdk": "^11.12.2",
22
+ "@salesforce/ui-bundle": "^11.12.2",
23
23
  "@tailwindcss/vite": "^4.1.17",
24
24
  "class-variance-authority": "^0.7.1",
25
25
  "clsx": "^2.1.1",
@@ -44,8 +44,8 @@
44
44
  "@graphql-eslint/eslint-plugin": "^4.1.0",
45
45
  "@graphql-tools/utils": "^11.0.0",
46
46
  "@playwright/test": "^1.49.0",
47
- "@salesforce/graphiti": "^11.12.0",
48
- "@salesforce/vite-plugin-ui-bundle": "^11.12.0",
47
+ "@salesforce/graphiti": "^11.12.2",
48
+ "@salesforce/vite-plugin-ui-bundle": "^11.12.2",
49
49
  "@testing-library/jest-dom": "^6.6.3",
50
50
  "@testing-library/react": "^16.1.0",
51
51
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1 @@
1
+ {"root":["./src/app.tsx","./src/appLayout.tsx","./src/navigationMenu.tsx","./src/router-utils.tsx","./src/routes.tsx","./src/api/graphqlClient.ts","./src/components/alerts/status-alert.tsx","./src/components/layouts/card-layout.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/datePicker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/field.tsx","./src/components/ui/index.ts","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/spinner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/features/search/index.ts","./src/features/search/loadConfig.ts","./src/features/search/queryBuilder.ts","./src/features/search/types.ts","./src/features/search/api/distinctValuesService.ts","./src/features/search/api/searchService.ts","./src/features/search/components/MergedSearchResults.tsx","./src/features/search/components/Search.tsx","./src/features/search/components/SearchResults.tsx","./src/features/search/components/SourceSection.tsx","./src/features/search/components/controls/PaginationControls.tsx","./src/features/search/components/controls/ScopeSelector.tsx","./src/features/search/components/controls/SearchBar.tsx","./src/features/search/components/controls/SortControl.tsx","./src/features/search/components/filters/ActiveFilters.tsx","./src/features/search/components/filters/DefaultFilterPanel.tsx","./src/features/search/components/filters/FilterContext.tsx","./src/features/search/components/filters/inputs/BooleanFilter.tsx","./src/features/search/components/filters/inputs/DateRangeFilter.tsx","./src/features/search/components/filters/inputs/FilterFieldWrapper.tsx","./src/features/search/components/filters/inputs/MultiSelectFilter.tsx","./src/features/search/components/filters/inputs/NumericRangeFilter.tsx","./src/features/search/components/filters/inputs/SelectFilter.tsx","./src/features/search/components/filters/inputs/TextFilter.tsx","./src/features/search/components/results/DefaultResultRow.tsx","./src/features/search/hooks/useAsyncData.ts","./src/features/search/hooks/useDistinctValues.ts","./src/features/search/hooks/useSearch.ts","./src/features/search/utils/debounce.ts","./src/features/search/utils/fieldUtils.ts","./src/features/search/utils/filterUtils.ts","./src/features/search/utils/sortUtils.ts","./src/hooks/useAsyncData.ts","./src/lib/utils.ts","./src/pages/Home.tsx","./src/pages/NotFound.tsx","./e2e/app.spec.ts","./vite-env.d.ts","./vitest-env.d.ts","./vitest.setup.ts","../../../../../../../../../sdk/platform-sdk/types/env.d.ts"],"version":"5.9.3"}
@@ -1,13 +1,17 @@
1
1
  {
2
- "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "11.12.0",
2
+ "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
+ "version": "11.12.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
9
- "version": "11.12.0",
8
+ "name": "@salesforce/ui-bundle-template-base-sfdx-project",
9
+ "version": "11.12.2",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
+ "dependencies": {
12
+ "fast-xml-parser": "^5.9.3",
13
+ "zod": "^3.24.1"
14
+ },
11
15
  "devDependencies": {
12
16
  "@lwc/eslint-plugin-lwc": "^3.3.0",
13
17
  "@prettier/plugin-xml": "^3.2.2",
@@ -2044,6 +2048,18 @@
2044
2048
  "eslint-scope": "5.1.1"
2045
2049
  }
2046
2050
  },
2051
+ "node_modules/@nodable/entities": {
2052
+ "version": "2.2.0",
2053
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz",
2054
+ "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==",
2055
+ "funding": [
2056
+ {
2057
+ "type": "github",
2058
+ "url": "https://github.com/sponsors/nodable"
2059
+ }
2060
+ ],
2061
+ "license": "MIT"
2062
+ },
2047
2063
  "node_modules/@nodelib/fs.scandir": {
2048
2064
  "version": "2.1.5",
2049
2065
  "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2898,6 +2914,18 @@
2898
2914
  "node": ">= 8"
2899
2915
  }
2900
2916
  },
2917
+ "node_modules/anynum": {
2918
+ "version": "1.0.1",
2919
+ "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
2920
+ "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
2921
+ "funding": [
2922
+ {
2923
+ "type": "github",
2924
+ "url": "https://github.com/sponsors/NaturalIntelligence"
2925
+ }
2926
+ ],
2927
+ "license": "MIT"
2928
+ },
2901
2929
  "node_modules/argparse": {
2902
2930
  "version": "2.0.1",
2903
2931
  "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4797,10 +4825,9 @@
4797
4825
  "license": "MIT"
4798
4826
  },
4799
4827
  "node_modules/fast-xml-builder": {
4800
- "version": "1.1.4",
4801
- "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
4802
- "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
4803
- "dev": true,
4828
+ "version": "1.2.0",
4829
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
4830
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
4804
4831
  "funding": [
4805
4832
  {
4806
4833
  "type": "github",
@@ -4809,14 +4836,14 @@
4809
4836
  ],
4810
4837
  "license": "MIT",
4811
4838
  "dependencies": {
4812
- "path-expression-matcher": "^1.1.3"
4839
+ "path-expression-matcher": "^1.5.0",
4840
+ "xml-naming": "^0.1.0"
4813
4841
  }
4814
4842
  },
4815
4843
  "node_modules/fast-xml-parser": {
4816
- "version": "5.5.6",
4817
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz",
4818
- "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==",
4819
- "dev": true,
4844
+ "version": "5.9.3",
4845
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz",
4846
+ "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==",
4820
4847
  "funding": [
4821
4848
  {
4822
4849
  "type": "github",
@@ -4825,9 +4852,12 @@
4825
4852
  ],
4826
4853
  "license": "MIT",
4827
4854
  "dependencies": {
4828
- "fast-xml-builder": "^1.1.4",
4829
- "path-expression-matcher": "^1.1.3",
4830
- "strnum": "^2.1.2"
4855
+ "@nodable/entities": "^2.2.0",
4856
+ "fast-xml-builder": "^1.2.0",
4857
+ "is-unsafe": "^1.0.1",
4858
+ "path-expression-matcher": "^1.5.0",
4859
+ "strnum": "^2.4.1",
4860
+ "xml-naming": "^0.1.0"
4831
4861
  },
4832
4862
  "bin": {
4833
4863
  "fxparser": "src/cli/cli.js"
@@ -5971,6 +6001,18 @@
5971
6001
  "url": "https://github.com/sponsors/ljharb"
5972
6002
  }
5973
6003
  },
6004
+ "node_modules/is-unsafe": {
6005
+ "version": "1.0.1",
6006
+ "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz",
6007
+ "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==",
6008
+ "funding": [
6009
+ {
6010
+ "type": "github",
6011
+ "url": "https://github.com/sponsors/NaturalIntelligence"
6012
+ }
6013
+ ],
6014
+ "license": "MIT"
6015
+ },
5974
6016
  "node_modules/is-weakmap": {
5975
6017
  "version": "2.0.2",
5976
6018
  "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -7856,10 +7898,9 @@
7856
7898
  }
7857
7899
  },
7858
7900
  "node_modules/path-expression-matcher": {
7859
- "version": "1.1.3",
7860
- "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
7861
- "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
7862
- "dev": true,
7901
+ "version": "1.6.1",
7902
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz",
7903
+ "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==",
7863
7904
  "funding": [
7864
7905
  {
7865
7906
  "type": "github",
@@ -9075,17 +9116,19 @@
9075
9116
  }
9076
9117
  },
9077
9118
  "node_modules/strnum": {
9078
- "version": "2.2.0",
9079
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
9080
- "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
9081
- "dev": true,
9119
+ "version": "2.4.1",
9120
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
9121
+ "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
9082
9122
  "funding": [
9083
9123
  {
9084
9124
  "type": "github",
9085
9125
  "url": "https://github.com/sponsors/NaturalIntelligence"
9086
9126
  }
9087
9127
  ],
9088
- "license": "MIT"
9128
+ "license": "MIT",
9129
+ "dependencies": {
9130
+ "anynum": "^1.0.1"
9131
+ }
9089
9132
  },
9090
9133
  "node_modules/supports-color": {
9091
9134
  "version": "7.2.0",
@@ -9877,6 +9920,21 @@
9877
9920
  "node": ">=12"
9878
9921
  }
9879
9922
  },
9923
+ "node_modules/xml-naming": {
9924
+ "version": "0.1.0",
9925
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
9926
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
9927
+ "funding": [
9928
+ {
9929
+ "type": "github",
9930
+ "url": "https://github.com/sponsors/NaturalIntelligence"
9931
+ }
9932
+ ],
9933
+ "license": "MIT",
9934
+ "engines": {
9935
+ "node": ">=16.0.0"
9936
+ }
9937
+ },
9880
9938
  "node_modules/xmlchars": {
9881
9939
  "version": "2.2.0",
9882
9940
  "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
@@ -9990,6 +10048,15 @@
9990
10048
  "funding": {
9991
10049
  "url": "https://github.com/sponsors/sindresorhus"
9992
10050
  }
10051
+ },
10052
+ "node_modules/zod": {
10053
+ "version": "3.25.76",
10054
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
10055
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
10056
+ "license": "MIT",
10057
+ "funding": {
10058
+ "url": "https://github.com/sponsors/colinhacks"
10059
+ }
9993
10060
  }
9994
10061
  }
9995
10062
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.12.0",
3
+ "version": "11.12.2",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
@@ -24,6 +24,7 @@
24
24
  "setup": "node scripts/org-setup.mjs"
25
25
  },
26
26
  "dependencies": {
27
+ "fast-xml-parser": "^5.9.3",
27
28
  "zod": "^3.24.1"
28
29
  },
29
30
  "devDependencies": {
@@ -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
+ }
@@ -37,11 +37,26 @@
37
37
  */
38
38
 
39
39
  import { spawnSync, spawn as nodeSpawn } from 'node:child_process';
40
- import { resolve, dirname } from 'node:path';
40
+ import { resolve, dirname, join } from 'node:path';
41
41
  import { fileURLToPath } from 'node:url';
42
- import { readdirSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
42
+ import {
43
+ readdirSync,
44
+ existsSync,
45
+ readFileSync,
46
+ writeFileSync,
47
+ mkdtempSync,
48
+ rmSync,
49
+ openSync,
50
+ closeSync,
51
+ } from 'node:fs';
52
+ import { tmpdir } from 'node:os';
43
53
 
44
54
  import { validateConfig } from './org-setup-config-schema.mjs';
55
+ import {
56
+ addProfileToMemberGroups,
57
+ enableSelfRegInXml,
58
+ NetworkXmlError,
59
+ } from './org-setup-xml.mjs';
45
60
 
46
61
  const __dirname = dirname(fileURLToPath(import.meta.url));
47
62
  const ROOT = resolve(__dirname, '..');
@@ -53,6 +68,149 @@ const ROOT = resolve(__dirname, '..');
53
68
  */
54
69
  class StepError extends Error {}
55
70
 
71
+ const APEX_TMP_PREFIX = 'org-setup-';
72
+ // Legacy fixed-name temp files that older versions of this script wrote to ROOT
73
+ // and could leave behind on a hard kill. Swept at startup for backward cleanup.
74
+ const LEGACY_ROOT_TMP_FILES = ['.tmp-setup-selfreg.apex', '.tmp-setup-delete.apex'];
75
+
76
+ /**
77
+ * Remove leftover Apex temp artifacts from a prior hard-killed run (cleanup AT
78
+ * START). Two sources: per-run dirs under os.tmpdir() created by
79
+ * withApexTempDir, and legacy fixed-name files this script used to write to
80
+ * ROOT. Best-effort: a failure to remove one entry never blocks setup.
81
+ *
82
+ * Scoped to DIRECTORY entries only: the per-run artifacts are dirs created by
83
+ * withApexTempDir via mkdtempSync, whereas the per-org lock files share the same
84
+ * `org-setup-` prefix but are plain files (`org-setup-lock-<org>.lock`). Sweeping
85
+ * files too would delete a live run's lock before acquireOrgLock could see it,
86
+ * defeating the concurrency guard. The dir sweep is still intentionally broad
87
+ * across PIDs; the per-target-org lock acquired in main() is what makes that
88
+ * safe — only one live run per org.
89
+ */
90
+ function sweepStaleApexTempDirs() {
91
+ const base = tmpdir();
92
+ let entries = [];
93
+ try {
94
+ entries = readdirSync(base, { withFileTypes: true });
95
+ } catch {
96
+ /* tmpdir unreadable — nothing to sweep */
97
+ }
98
+ for (const entry of entries) {
99
+ if (entry.isDirectory() && entry.name.startsWith(APEX_TMP_PREFIX)) {
100
+ try {
101
+ rmSync(join(base, entry.name), { recursive: true, force: true });
102
+ } catch {
103
+ /* best effort */
104
+ }
105
+ }
106
+ }
107
+ for (const legacy of LEGACY_ROOT_TMP_FILES) {
108
+ const p = resolve(ROOT, legacy);
109
+ if (existsSync(p)) {
110
+ try {
111
+ rmSync(p, { force: true });
112
+ } catch {
113
+ /* best effort */
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Run `fn(writeApex)` with a private temp dir under os.tmpdir() that is ALWAYS
121
+ * removed afterwards (cleanup IN FINALLY), even when fn throws or
122
+ * the spawned Apex step fails. `writeApex(basename, contents)` writes a file in
123
+ * that dir and returns its absolute path. Replaces the old fixed-name
124
+ * `.tmp-setup-*.apex` files in ROOT, which collided across concurrent runs and
125
+ * leaked on throw.
126
+ */
127
+ function withApexTempDir(fn) {
128
+ const dir = mkdtempSync(join(tmpdir(), APEX_TMP_PREFIX));
129
+ try {
130
+ return fn((basename, contents) => {
131
+ const p = join(dir, basename);
132
+ writeFileSync(p, contents);
133
+ return p;
134
+ });
135
+ } finally {
136
+ try {
137
+ rmSync(dir, { recursive: true, force: true });
138
+ } catch {
139
+ /* best effort */
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Filesystem-safe lock path for a target org. */
145
+ function orgLockPath(targetOrg) {
146
+ const safe = String(targetOrg).replace(/[^a-zA-Z0-9._-]/g, '_');
147
+ return join(tmpdir(), `org-setup-lock-${safe}.lock`);
148
+ }
149
+
150
+ /**
151
+ * Liveness probe: signal 0 throws ESRCH if the pid is gone, EPERM if it exists
152
+ * but we can't signal it (still alive, owned by another user).
153
+ */
154
+ function isProcessAlive(pid) {
155
+ try {
156
+ process.kill(pid, 0);
157
+ return true;
158
+ } catch (e) {
159
+ return e.code === 'EPERM';
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Acquire a single-host advisory per-target-org lock. If a LIVE
165
+ * run already holds it, exit early with a clear message rather than interleaving
166
+ * destructive Apex (deletes/imports) against the same org. A lock left by a
167
+ * hard-killed run (dead pid) is reclaimed. Different-org runs use different lock
168
+ * files and proceed in parallel.
169
+ *
170
+ * Released via process.on('exit') — NOT a finally block — because main() exits
171
+ * through process.exit() (the runStep fail-fast path, the end-of-run summary,
172
+ * and the top-level .catch), and process.exit() does not run finally blocks.
173
+ */
174
+ function acquireOrgLock(targetOrg) {
175
+ const lockPath = orgLockPath(targetOrg);
176
+ if (existsSync(lockPath)) {
177
+ const holder = Number(readFileSync(lockPath, 'utf8').trim());
178
+ if (holder && isProcessAlive(holder)) {
179
+ console.error(
180
+ `\nAnother org-setup run (pid ${holder}) is already targeting "${targetOrg}".\n` +
181
+ `Wait for it to finish, or if it was killed, remove ${lockPath} and retry.`,
182
+ );
183
+ process.exit(1);
184
+ }
185
+ // Stale lock (holder dead / hard-killed) — reclaim it.
186
+ rmSync(lockPath, { force: true });
187
+ }
188
+ // O_EXCL ('wx') create closes the check-then-write race between two
189
+ // near-simultaneous runs: the loser gets EEXIST.
190
+ let fd;
191
+ try {
192
+ fd = openSync(lockPath, 'wx');
193
+ } catch (e) {
194
+ if (e.code === 'EEXIST') {
195
+ console.error(
196
+ `\nAnother org-setup run just acquired the lock for "${targetOrg}". Retry shortly.`,
197
+ );
198
+ process.exit(1);
199
+ }
200
+ throw e;
201
+ }
202
+ writeFileSync(fd, String(process.pid));
203
+ closeSync(fd);
204
+
205
+ process.on('exit', () => {
206
+ try {
207
+ rmSync(lockPath, { force: true });
208
+ } catch {
209
+ /* best effort */
210
+ }
211
+ });
212
+ }
213
+
56
214
  /**
57
215
  * npm strips .gitignore from published packages — generate them on first run.
58
216
  * Templates are stored in scripts/gitignore-templates.json (generated at build
@@ -254,7 +412,7 @@ function loadValidatedConfig() {
254
412
  /**
255
413
  * Derive the site name from the single networks/<siteName>.network-meta.xml the
256
414
  * app ships. An app ships exactly one site, so the site name is derivable from
257
- * deployed metadata rather than restated per-assignment (spec §5.2). Returns
415
+ * deployed metadata rather than restated per-assignment. Returns
258
416
  * null when there is no networks dir or no .network-meta.xml file.
259
417
  */
260
418
  function deriveSiteName() {
@@ -335,7 +493,7 @@ function loadRoleConfig(config) {
335
493
  /**
336
494
  * Self-registration config, read from the already-validated config. The site is
337
495
  * NOT stored here — it is derived from the single
338
- * networks/<siteName>.network-meta.xml the app ships (spec §5.2), exactly like
496
+ * networks/<siteName>.network-meta.xml the app ships, exactly like
339
497
  * the guestUser permset path. deriveSiteName() is called lazily inside the
340
498
  * selfReg step body so its "multiple network files" StepError is recorded in
341
499
  * the ledger rather than escaping config load.
@@ -375,20 +533,31 @@ function ensureNetworkMemberProfile(selfRegConfig, siteName) {
375
533
  }
376
534
  const xml = readFileSync(networkXmlPath, 'utf8');
377
535
 
378
- // Check if profile is already in networkMemberGroups
379
- const profileEscaped = selfRegProfile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
380
- const profileRegex = new RegExp(`<profile>\\s*${profileEscaped}\\s*</profile>`);
381
- if (profileRegex.test(xml)) {
536
+ // Parse + assert the target node exists, then mutate via a targeted edit.
537
+ // This is the BEST-EFFORT pre-deploy prep (not inside runStep, see lines
538
+ // ~1050): on a missing <networkMemberGroups> node we surface a LOUD
539
+ // console.error but do NOT throw — a bare throw here aborts before deploy and
540
+ // bypasses the ledger. The authoritative failure is recorded later by the
541
+ // post-deploy selfReg step.
542
+ let result;
543
+ try {
544
+ result = addProfileToMemberGroups(xml, selfRegProfile);
545
+ } catch (e) {
546
+ if (e instanceof NetworkXmlError) {
547
+ console.error(
548
+ ` ERROR: cannot add self-reg profile to ${siteName}.network-meta.xml — ${e.message}. ` +
549
+ `Continuing pre-deploy; the self-registration step will record the authoritative failure.`,
550
+ );
551
+ return;
552
+ }
553
+ throw e;
554
+ }
555
+
556
+ if (!result.changed) {
382
557
  console.log(` Profile "${selfRegProfile}" already in networkMemberGroups; no update needed.`);
383
558
  return;
384
559
  }
385
-
386
- // Add the profile to networkMemberGroups
387
- const updatedXml = xml.replace(
388
- /(<networkMemberGroups>)/,
389
- `$1\n <profile>${selfRegProfile}</profile>`
390
- );
391
- writeFileSync(networkXmlPath, updatedXml);
560
+ writeFileSync(networkXmlPath, result.xml);
392
561
  console.log(` Added profile "${selfRegProfile}" to networkMemberGroups in ${siteName}.network-meta.xml`);
393
562
  }
394
563
 
@@ -410,23 +579,26 @@ function enableSelfRegistration(selfRegConfig, siteName, targetOrg) {
410
579
  }
411
580
  const xml = readFileSync(networkXmlPath, 'utf8');
412
581
 
413
- // Skip network modification and deploy if self-registration is already configured
414
- const alreadyEnabled = /<selfRegistration>true<\/selfRegistration>/.test(xml);
415
- const alreadyHasProfile = /<selfRegProfile>/.test(xml);
416
- if (alreadyEnabled || alreadyHasProfile) {
582
+ // Parse + assert the <selfRegistration> node exists, then mutate via a
583
+ // targeted edit. This runs inside
584
+ // runStep(selfReg, failFast:false) (line ~1211), so a missing node must throw
585
+ // a StepError caught, recorded `failed`, non-zero exit — rather than the
586
+ // old silent no-op. The helper's idempotency check
587
+ // (already true / selfRegProfile present) yields changed:false here.
588
+ let result;
589
+ try {
590
+ result = enableSelfRegInXml(xml, selfRegProfile);
591
+ } catch (e) {
592
+ if (e instanceof NetworkXmlError) {
593
+ throw new StepError(`${siteName}.network-meta.xml: ${e.message}`);
594
+ }
595
+ throw e;
596
+ }
597
+
598
+ if (!result.changed) {
417
599
  console.log(` Network "${siteName}" already has self-registration configured; skipping metadata update and deploy.`);
418
600
  } else {
419
- // Set selfRegistration to true and add selfRegProfile
420
- let updatedXml = xml.replace(
421
- /<selfRegistration>false<\/selfRegistration>/,
422
- '<selfRegistration>true</selfRegistration>'
423
- );
424
- updatedXml = updatedXml.replace(
425
- /(\s*)(<selfRegistration>)/,
426
- `$1<selfRegProfile>${selfRegProfile}</selfRegProfile>\n$1$2`
427
- );
428
-
429
- writeFileSync(networkXmlPath, updatedXml);
601
+ writeFileSync(networkXmlPath, result.xml);
430
602
  console.log(` Updated ${siteName}.network-meta.xml: selfRegistration=true, selfRegProfile=${selfRegProfile}`);
431
603
 
432
604
  // Re-deploy only the network file
@@ -516,7 +688,6 @@ function enableSelfRegistration(selfRegConfig, siteName, targetOrg) {
516
688
  if (nsrExists) {
517
689
  console.log(' NetworkSelfRegistration record already exists; skipping.');
518
690
  } else {
519
- const tmpApex = resolve(ROOT, '.tmp-setup-selfreg.apex');
520
691
  const apex = [
521
692
  `Account acct = [SELECT Id FROM Account WHERE Id = '${accountId}' LIMIT 1];`,
522
693
  `NetworkSelfRegistration nsr = new NetworkSelfRegistration();`,
@@ -525,16 +696,18 @@ function enableSelfRegistration(selfRegConfig, siteName, targetOrg) {
525
696
  `insert nsr;`,
526
697
  `System.debug('NSR_CREATED:' + nsr.Id);`,
527
698
  ].join('\n');
528
- writeFileSync(tmpApex, apex);
529
- const apexResult = spawnSync('sf', [
530
- 'apex', 'run', '--target-org', targetOrg, '--file', tmpApex,
531
- ], { cwd: ROOT, stdio: 'pipe', shell: true, timeout: 60000 });
532
- const apexOut = apexResult.stdout?.toString() || '';
533
- if (existsSync(tmpApex)) unlinkSync(tmpApex);
534
- if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
535
- process.stderr.write(apexResult.stderr?.toString() || apexOut);
536
- throw new StepError('failed to create NetworkSelfRegistration record');
537
- }
699
+ const apexOut = withApexTempDir((writeApex) => {
700
+ const tmpApex = writeApex('selfreg.apex', apex);
701
+ const apexResult = spawnSync('sf', [
702
+ 'apex', 'run', '--target-org', targetOrg, '--file', tmpApex,
703
+ ], { cwd: ROOT, stdio: 'pipe', shell: true, timeout: 60000 });
704
+ const out = apexResult.stdout?.toString() || '';
705
+ if (apexResult.status !== 0 && !out.includes('Compiled successfully')) {
706
+ process.stderr.write(apexResult.stderr?.toString() || out);
707
+ throw new StepError('failed to create NetworkSelfRegistration record');
708
+ }
709
+ return out;
710
+ });
538
711
  const nsrMatch = apexOut.match(/NSR_CREATED:(\w+)/);
539
712
  if (nsrMatch) {
540
713
  console.log(` Created NetworkSelfRegistration (${nsrMatch[1]}).`);
@@ -931,6 +1104,13 @@ async function runStep(step, targetOrg, body) {
931
1104
  }
932
1105
 
933
1106
  async function main() {
1107
+ // Cleanup AT START: remove Apex temp artifacts left by a prior hard-killed run.
1108
+ // Safe to run before the per-org lock is acquired because it only removes
1109
+ // org-setup-* temp DIRECTORIES and legacy ROOT temp files — never the
1110
+ // org-setup-lock-<org>.lock files (plain files), so it cannot clobber a
1111
+ // concurrent run's live lock.
1112
+ sweepStaleApexTempDirs();
1113
+
934
1114
  // Ensure .gitignore files exist (npm strips them from published packages).
935
1115
  const gitignoreTemplates = loadGitignoreTemplates();
936
1116
  if (gitignoreTemplates) {
@@ -960,6 +1140,12 @@ async function main() {
960
1140
  skipDev: argSkipDev,
961
1141
  } = parseArgs();
962
1142
 
1143
+ // Per-target-org lock: acquired right after the start-of-run
1144
+ // sweep and before any step runs, so two runs against the same org cannot
1145
+ // interleave destructive Apex. The startup sweep above deliberately skips lock
1146
+ // files, so it never races this. Released on process exit.
1147
+ acquireOrgLock(targetOrg);
1148
+
963
1149
  const permsetNames =
964
1150
  permsetNamesExplicit.length > 0 ? permsetNamesExplicit : discoverPermissionSetNames();
965
1151
  const permsetStepLabel =
@@ -969,8 +1155,8 @@ async function main() {
969
1155
  ? `Permset — assign ${permsetNames.join(', ')}`
970
1156
  : `Permset — assign ${permsetNames.length} permission sets`;
971
1157
 
972
- // Validate org-setup.config.json ONCE, before any step runs (spec §5.2). The
973
- // three load*Config helpers read from this already-validated object.
1158
+ // Validate org-setup.config.json ONCE, before any step runs. The three
1159
+ // load*Config helpers read from this already-validated object.
974
1160
  const config = loadValidatedConfig();
975
1161
 
976
1162
  const hasDataPlan = existsSync(DATA_PLAN) && existsSync(DATA_DIR);
@@ -979,7 +1165,7 @@ async function main() {
979
1165
  const selfRegConfig = loadSelfRegConfig(config);
980
1166
  const hasSelfRegConfig = selfRegConfig !== null;
981
1167
 
982
- // failFast is a fixed, implementer-owned classification (spec §5.2) — NOT
1168
+ // failFast is a fixed, implementer-owned classification — NOT
983
1169
  // user-configurable. A fail-fast failure aborts the run immediately; a
984
1170
  // skippable failure is recorded and the run continues. The exit code is
985
1171
  // non-zero on any failure regardless of class.
@@ -1036,7 +1222,7 @@ async function main() {
1036
1222
  console.log('\n--- Login ---');
1037
1223
  console.log(`Org ${targetOrg} is already authenticated; skipping browser login.`);
1038
1224
  } else {
1039
- // Login is fail-fast (spec §5.2): drop the previous { optional: true } so a
1225
+ // Login is fail-fast: drop the previous { optional: true } so a
1040
1226
  // failed browser login records `failed`, prints the summary, and aborts.
1041
1227
  run('Login (browser)', 'sf', ['org', 'login', 'web', '--alias', targetOrg]);
1042
1228
  }
@@ -1126,7 +1312,7 @@ async function main() {
1126
1312
  let effectiveUsername = null;
1127
1313
  if (assignment.assignee === 'guestUser') {
1128
1314
  // Site name is derived from the single network metadata file the app
1129
- // ships (spec §5.2) — never restated per-assignment.
1315
+ // ships — never restated per-assignment.
1130
1316
  const siteName = deriveSiteName();
1131
1317
  if (!siteName) {
1132
1318
  console.error(`Permission set "${permsetName}" — assignee is "guestUser" but no networks/<siteName>.network-meta.xml was found to derive the site; skipping.`);
@@ -1233,7 +1419,9 @@ async function main() {
1233
1419
  console.log('\n--- Clean existing data for fresh import ---');
1234
1420
  const planEntries = JSON.parse(readFileSync(DATA_PLAN, 'utf8'));
1235
1421
  const sobjectsReversed = [...planEntries.map((e) => e.sobject)].reverse();
1236
- const tmpApex = resolve(ROOT, '.tmp-setup-delete.apex');
1422
+ // One private temp dir for both the delete sweep and the import batches;
1423
+ // always removed in finally, even if a batch throws.
1424
+ withApexTempDir((writeApex) => {
1237
1425
  for (const sobject of sobjectsReversed) {
1238
1426
  const apexCode = [
1239
1427
  'try {',
@@ -1246,7 +1434,7 @@ async function main() {
1246
1434
  ' // non-deletable records (e.g. Contact linked to Case) are skipped via allOrNone=false',
1247
1435
  '}',
1248
1436
  ].join('\n');
1249
- writeFileSync(tmpApex, apexCode);
1437
+ const tmpApex = writeApex('data.apex', apexCode);
1250
1438
  spawnSync('sf', ['apex', 'run', '--target-org', targetOrg, '--file', tmpApex], {
1251
1439
  cwd: ROOT,
1252
1440
  stdio: 'pipe',
@@ -1255,7 +1443,6 @@ async function main() {
1255
1443
  });
1256
1444
  console.log(` ${sobject}: cleaned`);
1257
1445
  }
1258
- if (existsSync(tmpApex)) unlinkSync(tmpApex);
1259
1446
 
1260
1447
  // Import via Anonymous Apex with Database.DMLOptions.duplicateRuleHeader.allowSave = true.
1261
1448
  // This bypasses both duplicate-rule blocks AND matching-service timeouts that the REST
@@ -1294,7 +1481,7 @@ async function main() {
1294
1481
  const batch = records.slice(i, i + batchSize);
1295
1482
  const refIds = batch.map((r) => r.attributes?.referenceId || `_idx${i}`);
1296
1483
  const apex = buildApexInsert(entry.sobject, batch, refIds);
1297
- writeFileSync(tmpApex, apex);
1484
+ const tmpApex = writeApex('data.apex', apex);
1298
1485
  const apexResult = spawnSync(
1299
1486
  'sf',
1300
1487
  ['apex', 'run', '--target-org', targetOrg, '--file', tmpApex],
@@ -1323,7 +1510,7 @@ async function main() {
1323
1510
  console.log(` ${entry.sobject}: imported ${imported} records`);
1324
1511
  }
1325
1512
  }
1326
- if (existsSync(tmpApex)) unlinkSync(tmpApex);
1513
+ });
1327
1514
  });
1328
1515
  } else {
1329
1516
  recordSkipped(dataStep, dataStep.available ? 'not selected' : 'no data plan');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-feature-react-search",
3
- "version": "11.12.0",
3
+ "version": "11.12.2",
4
4
  "description": "[Beta] Configuration-driven multi-source search for UI Bundles",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",