@salesforce/ui-bundle-template-feature-react-global-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 +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.tsbuildinfo +1 -0
- package/dist/package-lock.json +92 -25
- package/dist/package.json +2 -1
- package/dist/scripts/org-setup-xml.mjs +182 -0
- package/dist/scripts/org-setup.mjs +238 -51
- package/package.json +1 -1
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.
|
|
22
|
-
"@salesforce/ui-bundle": "^11.12.
|
|
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",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
47
47
|
"@graphql-tools/utils": "^11.0.0",
|
|
48
48
|
"@playwright/test": "^1.49.0",
|
|
49
|
-
"@salesforce/graphiti": "^11.12.
|
|
50
|
-
"@salesforce/vite-plugin-ui-bundle": "^11.12.
|
|
49
|
+
"@salesforce/graphiti": "^11.12.2",
|
|
50
|
+
"@salesforce/vite-plugin-ui-bundle": "^11.12.2",
|
|
51
51
|
"@testing-library/jest-dom": "^6.6.3",
|
|
52
52
|
"@testing-library/react": "^16.1.0",
|
|
53
53
|
"@testing-library/user-event": "^14.5.2",
|
package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.tsbuildinfo
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/app.tsx","./src/appLayout.tsx","./src/index.ts","./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/global-search/constants.ts","./src/features/global-search/api/objectDetailService.ts","./src/features/global-search/api/objectInfoGraphQLService.ts","./src/features/global-search/api/objectInfoService.ts","./src/features/global-search/api/recordListGraphQLService.ts","./src/features/global-search/components/detail/DetailFields.tsx","./src/features/global-search/components/detail/DetailForm.tsx","./src/features/global-search/components/detail/DetailHeader.tsx","./src/features/global-search/components/detail/DetailLayoutSections.tsx","./src/features/global-search/components/detail/Section.tsx","./src/features/global-search/components/detail/SectionRow.tsx","./src/features/global-search/components/detail/UiApiDetailForm.tsx","./src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx","./src/features/global-search/components/detail/formatted/FormattedAddress.tsx","./src/features/global-search/components/detail/formatted/FormattedEmail.tsx","./src/features/global-search/components/detail/formatted/FormattedPhone.tsx","./src/features/global-search/components/detail/formatted/FormattedText.tsx","./src/features/global-search/components/detail/formatted/FormattedUrl.tsx","./src/features/global-search/components/filters/FilterField.tsx","./src/features/global-search/components/filters/FilterInput.tsx","./src/features/global-search/components/filters/FilterSelect.tsx","./src/features/global-search/components/filters/FiltersPanel.tsx","./src/features/global-search/components/forms/filters-form.tsx","./src/features/global-search/components/forms/submit-button.tsx","./src/features/global-search/components/search/GlobalSearchInput.tsx","./src/features/global-search/components/search/ResultCardFields.tsx","./src/features/global-search/components/search/SearchHeader.tsx","./src/features/global-search/components/search/SearchPagination.tsx","./src/features/global-search/components/search/SearchResultCard.tsx","./src/features/global-search/components/search/SearchResultsPanel.tsx","./src/features/global-search/components/shared/LoadingFallback.tsx","./src/features/global-search/filters/FilterInput.tsx","./src/features/global-search/filters/FilterSelect.tsx","./src/features/global-search/hooks/form.tsx","./src/features/global-search/hooks/useObjectInfoBatch.ts","./src/features/global-search/hooks/useObjectSearchData.ts","./src/features/global-search/hooks/useRecordDetailLayout.ts","./src/features/global-search/hooks/useRecordListGraphQL.ts","./src/features/global-search/pages/DetailPage.tsx","./src/features/global-search/pages/GlobalSearch.tsx","./src/features/global-search/types/schema.d.ts","./src/features/global-search/types/filters/filters.ts","./src/features/global-search/types/filters/picklist.ts","./src/features/global-search/types/objectInfo/objectInfo.ts","./src/features/global-search/types/recordDetail/recordDetail.ts","./src/features/global-search/types/search/searchResults.ts","./src/features/global-search/utils/apiUtils.ts","./src/features/global-search/utils/cacheUtils.ts","./src/features/global-search/utils/debounce.ts","./src/features/global-search/utils/fieldUtils.ts","./src/features/global-search/utils/fieldValueExtractor.ts","./src/features/global-search/utils/filterUtils.ts","./src/features/global-search/utils/formDataTransformUtils.ts","./src/features/global-search/utils/formUtils.ts","./src/features/global-search/utils/graphQLNodeFieldUtils.ts","./src/features/global-search/utils/graphQLObjectInfoAdapter.ts","./src/features/global-search/utils/graphQLRecordAdapter.ts","./src/features/global-search/utils/layoutTransformUtils.ts","./src/features/global-search/utils/linkUtils.ts","./src/features/global-search/utils/paginationUtils.ts","./src/features/global-search/utils/recordUtils.ts","./src/features/global-search/utils/sanitizationUtils.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"}
|
package/dist/package-lock.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@salesforce/
|
|
3
|
-
"version": "11.12.
|
|
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/
|
|
9
|
-
"version": "11.12.
|
|
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.
|
|
4801
|
-
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.
|
|
4802
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
4817
|
-
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.
|
|
4818
|
-
"integrity": "sha512-
|
|
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
|
-
"
|
|
4829
|
-
"
|
|
4830
|
-
"
|
|
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
|
|
7860
|
-
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.
|
|
7861
|
-
"integrity": "sha512-
|
|
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.
|
|
9079
|
-
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.
|
|
9080
|
-
"integrity": "sha512-
|
|
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.
|
|
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 `&`-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 {
|
|
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
|
|
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
|
|
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
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
973
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-global-search",
|
|
3
|
-
"version": "11.12.
|
|
3
|
+
"version": "11.12.2",
|
|
4
4
|
"description": "Global search feature for Salesforce objects with filtering and pagination",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|