@salesforce/packaging 2.3.0 → 2.3.1

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.
@@ -4,6 +4,7 @@ import { SaveResult } from 'jsforce';
4
4
  import { Attributes } from 'graphology-types';
5
5
  import { Optional } from '@salesforce/ts-types';
6
6
  import { ConvertResult } from '@salesforce/source-deploy-retrieve';
7
+ import { Package } from 'jsforce/lib/api/metadata';
7
8
  import { PackageProfileApi } from '../package/packageProfileApi';
8
9
  import { PackageAncestryNode } from '../package/packageAncestry';
9
10
  import { PackagingSObjects } from './packagingSObjects';
@@ -272,7 +273,6 @@ export type PackageVersionCreateRequestQueryOptions = {
272
273
  export type ProfileApiOptions = {
273
274
  project: SfProject;
274
275
  includeUserLicenses: boolean;
275
- generateProfileInformation: boolean;
276
276
  };
277
277
  export type PackageVersionReportResult = Partial<Omit<PackagingSObjects.Package2Version, 'AncestorId' | 'HasPassedCodeCoverageCheck' | 'HasMetadataRemoved'>> & {
278
278
  Package2: Partial<Omit<PackagingSObjects.Package2, 'IsOrgDependent'>> & {
@@ -373,3 +373,4 @@ export declare const Package1VersionEvents: {
373
373
  progress: string;
374
374
  };
375
375
  };
376
+ export type PackageXml = Pick<Package, 'types' | 'version'>;
@@ -1,63 +1,27 @@
1
1
  import { SfProject } from '@salesforce/core';
2
2
  import { AsyncCreatable } from '@salesforce/kit';
3
- import { ProfileApiOptions } from '../interfaces';
3
+ import { PackageXml, ProfileApiOptions } from '../interfaces';
4
4
  export declare class PackageProfileApi extends AsyncCreatable<ProfileApiOptions> {
5
- readonly profiles: ProfileInformation[];
6
- nodeEntities: {
7
- name: string[];
8
- parentElement: string[];
9
- childElement: string[];
10
- };
11
- otherProfileSettings: {
12
- name: string[];
13
- parentElement: string[];
14
- childElement: string[];
15
- };
16
5
  project: SfProject;
17
6
  includeUserLicenses: boolean;
18
- generateProfileInformation: boolean;
19
7
  constructor(options: ProfileApiOptions);
20
8
  init(): Promise<void>;
21
9
  /**
22
10
  * For any profile present in the workspace, this function generates a subset of data that only contains references
23
11
  * to items in the manifest.
24
12
  *
13
+ * return a list of profile file locations that need to be removed from the package because they are empty
14
+ *
25
15
  * @param destPath location of new profiles
26
- * @param manifest
16
+ * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
27
17
  * @param excludedDirectories Directories to not include profiles from
28
18
  */
29
- generateProfiles(destPath: string, manifest: {
30
- Package: Array<{
31
- name: string[];
32
- members: string[];
33
- }>;
34
- }, excludedDirectories?: string[]): string[];
19
+ generateProfiles(destPath: string, manifestTypes: PackageXml['types'], excludedDirectories?: string[]): string[];
35
20
  /**
36
21
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
37
22
  *
38
23
  * @param typesArr array of objects { name[], members[] } that represent package types JSON.
39
24
  * @param excludedDirectories Direcotires not to generate profiles for
40
25
  */
41
- filterAndGenerateProfilesForManifest(typesArr: Array<{
42
- name: string[];
43
- members: string[];
44
- }>, excludedDirectories?: string[]): Array<{
45
- name: string[];
46
- members: string[];
47
- }>;
48
- getProfileInformation(): ProfileInformation[];
49
- private copyNodes;
50
- private findAllProfiles;
51
- }
52
- declare class ProfileInformation {
53
- ProfileName: string;
54
- ProfilePath: string;
55
- IsPackaged: boolean;
56
- settingsRemoved: string[];
57
- constructor(ProfileName: string, ProfilePath: string, IsPackaged: boolean, settingsRemoved: string[]);
58
- setIsPackaged(IsPackaged: boolean): void;
59
- appendRemovedSetting(setting: string): void;
60
- logDebug(): string;
61
- logInfo(): string;
26
+ filterAndGenerateProfilesForManifest(typesArr: PackageXml['types'], excludedDirectories?: string[]): PackageXml['types'];
62
27
  }
63
- export {};
@@ -1,77 +1,20 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackageProfileApi = void 0;
2
4
  /*
3
5
  * Copyright (c) 2022, salesforce.com, inc.
4
6
  * All rights reserved.
5
7
  * Licensed under the BSD 3-Clause license.
6
8
  * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
9
  */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.PackageProfileApi = void 0;
10
10
  const path = require("path");
11
11
  const fs = require("fs");
12
12
  const glob = require("glob");
13
- const xmldom_1 = require("@xmldom/xmldom");
14
13
  const core_1 = require("@salesforce/core");
15
14
  const kit_1 = require("@salesforce/kit");
15
+ const profileRewriter_1 = require("./profileRewriter");
16
16
  core_1.Messages.importMessagesDirectory(__dirname);
17
17
  const profileApiMessages = core_1.Messages.loadMessages('@salesforce/packaging', 'profile_api');
18
- // nodeEntities is used to determine which elements in the profile are relevant to the source being packaged.
19
- // name refers to the entity type name in source that the element pertains to. As an example, a profile may
20
- // have an entry like the example below, which should only be added to the packaged profile if the related
21
- // CustomObject is in the source being packaged:
22
- // <objectPermissions>
23
- // <allowCreate>true</allowCreate>
24
- // ...
25
- // <object>MyCustomObject__c</object>
26
- // ...
27
- // </objectPermissions>
28
- //
29
- // For this example: nodeEntities.parentElement = objectPermissions and nodeEntities.childElement = object
30
- const NODE_ENTITIES = {
31
- name: [
32
- 'CustomObject',
33
- 'CustomField',
34
- 'Layout',
35
- 'CustomTab',
36
- 'CustomApplication',
37
- 'ApexClass',
38
- 'CustomPermission',
39
- 'ApexPage',
40
- 'ExternalDataSource',
41
- 'RecordType',
42
- ],
43
- parentElement: [
44
- 'objectPermissions',
45
- 'fieldPermissions',
46
- 'layoutAssignments',
47
- 'tabVisibilities',
48
- 'applicationVisibilities',
49
- 'classAccesses',
50
- 'customPermissions',
51
- 'pageAccesses',
52
- 'externalDataSourceAccesses',
53
- 'recordTypeVisibilities',
54
- ],
55
- childElement: [
56
- 'object',
57
- 'field',
58
- 'layout',
59
- 'tab',
60
- 'application',
61
- 'apexClass',
62
- 'name',
63
- 'apexPage',
64
- 'externalDataSource',
65
- 'recordType',
66
- ],
67
- };
68
- // There are some profile settings that are allowed to be packaged that may not necessarily map to a specific metadata
69
- // object. We should still handle these accordingly, but a little differently than the above mentioned types.
70
- const OTHER_PROFILE_SETTINGS = {
71
- name: ['CustomSettings', 'CustomMetadataTypeAccess'],
72
- parentElement: ['customSettingAccesses', 'customMetadataTypeAccesses'],
73
- childElement: ['name', 'name'],
74
- };
75
18
  /*
76
19
  * This class provides functions used to re-write .profiles in the workspace when creating a package2 version.
77
20
  * All profiles found in the workspaces are extracted out and then re-written to only include metadata in the profile
@@ -80,14 +23,9 @@ const OTHER_PROFILE_SETTINGS = {
80
23
  class PackageProfileApi extends kit_1.AsyncCreatable {
81
24
  constructor(options) {
82
25
  super(options);
83
- this.profiles = [];
84
- this.nodeEntities = NODE_ENTITIES;
85
- this.otherProfileSettings = OTHER_PROFILE_SETTINGS;
86
26
  this.includeUserLicenses = false;
87
- this.generateProfileInformation = false;
88
27
  this.project = options.project;
89
28
  this.includeUserLicenses = options.includeUserLicenses ?? false;
90
- this.generateProfileInformation = options.generateProfileInformation ?? false;
91
29
  }
92
30
  // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function
93
31
  async init() { }
@@ -95,91 +33,45 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
95
33
  * For any profile present in the workspace, this function generates a subset of data that only contains references
96
34
  * to items in the manifest.
97
35
  *
36
+ * return a list of profile file locations that need to be removed from the package because they are empty
37
+ *
98
38
  * @param destPath location of new profiles
99
- * @param manifest
39
+ * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
100
40
  * @param excludedDirectories Directories to not include profiles from
101
41
  */
102
- generateProfiles(destPath, manifest, excludedDirectories = []) {
103
- const excludedProfiles = [];
104
- const profilePaths = this.findAllProfiles(excludedDirectories);
105
- if (!profilePaths) {
106
- return excludedProfiles;
107
- }
108
- profilePaths.forEach((profilePath) => {
109
- // profile metadata can present in any directory in the package structure
110
- const profileNameMatch = profilePath.match(/([^/]+)\.profile-meta.xml/);
111
- const profileName = profileNameMatch ? profileNameMatch[1] : null;
112
- if (profileName) {
113
- const profileDom = new xmldom_1.DOMParser().parseFromString(fs.readFileSync(profilePath, 'utf-8'));
114
- const newDom = new xmldom_1.DOMParser().parseFromString('<?xml version="1.0" encoding="UTF-8"?><Profile xmlns="http://soap.sforce.com/2006/04/metadata"></Profile>');
115
- const profileNode = newDom.getElementsByTagName('Profile')[0];
116
- let hasNodes = false;
117
- // We need to keep track of all the members for when we package up the "OtherProfileSettings"
118
- let allMembers = [];
119
- manifest.Package.forEach((element) => {
120
- const name = element.name;
121
- const members = element.members;
122
- allMembers = allMembers.concat(members);
123
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
124
- const idx = this.nodeEntities.name.indexOf(name[0]);
125
- if (idx > -1) {
126
- hasNodes =
127
- this.copyNodes(profileDom, this.nodeEntities.parentElement[idx], this.nodeEntities.childElement[idx], members, profileNode, profileName) || hasNodes;
128
- }
129
- });
130
- // Go through each of the other profile settings we might want to include. We pass in "all" the members since these
131
- // will reference anything that could be packaged. The "copyNodes" function will only include the type if it
132
- // exists in the profile itself
133
- this.otherProfileSettings.name.forEach((element) => {
134
- const idx = this.otherProfileSettings.name.indexOf(element);
135
- if (idx > -1) {
136
- hasNodes =
137
- this.copyNodes(profileDom, this.otherProfileSettings.parentElement[idx], this.otherProfileSettings.childElement[idx], allMembers, profileNode, profileName) || hasNodes;
138
- }
42
+ generateProfiles(destPath, manifestTypes, excludedDirectories = []) {
43
+ const logger = core_1.Logger.childFromRoot('PackageProfileApi');
44
+ return (getProfilesWithNamesAndPaths({
45
+ projectPath: this.project.getPath(),
46
+ excludedDirectories,
47
+ })
48
+ .map(({ profilePath, name: profileName }) => {
49
+ const originalProfile = (0, profileRewriter_1.profileStringToProfile)(fs.readFileSync(profilePath, 'utf-8'));
50
+ const adjustedProfile = (0, profileRewriter_1.profileRewriter)(originalProfile, (0, profileRewriter_1.manifestTypesToMap)(manifestTypes), this.includeUserLicenses);
51
+ return {
52
+ profileName,
53
+ profilePath,
54
+ hasContent: Object.keys(adjustedProfile).length,
55
+ adjustedProfile,
56
+ removedSettings: getRemovedSettings(originalProfile, adjustedProfile),
57
+ xmlFileLocation: getXmlFileLocation(destPath, profilePath),
58
+ };
59
+ })
60
+ // side effect: modify profiles in place
61
+ .filter(({ hasContent, profileName, removedSettings, profilePath, xmlFileLocation, adjustedProfile }) => {
62
+ if (!hasContent) {
63
+ logger.warn(`Profile ${profileName} has no content after filtering. It will still be part of the package but you can remove if it it's not needed.`);
64
+ return true;
65
+ }
66
+ else {
67
+ logger.info(profileApiMessages.getMessage('addProfileToPackage', [profileName, profilePath]));
68
+ removedSettings.forEach((setting) => {
69
+ logger.info(profileApiMessages.getMessage('removeProfileSetting', [setting, profileName]));
139
70
  });
140
- // add userLicenses to the profile
141
- if (this.includeUserLicenses) {
142
- const userLicenses = profileDom.getElementsByTagName('userLicense');
143
- if (userLicenses) {
144
- hasNodes = true;
145
- for (const userLicense of Array.from(userLicenses)) {
146
- profileNode.appendChild(userLicense.cloneNode(true));
147
- }
148
- }
149
- }
150
- const xmlSrcFile = path.basename(profilePath);
151
- const xmlFile = xmlSrcFile.replace(/(.*)(-meta.xml)/, '$1');
152
- const destFilePath = path.join(destPath, xmlFile);
153
- if (hasNodes) {
154
- const serializer = new xmldom_1.XMLSerializer();
155
- serializer.serializeToString(newDom);
156
- fs.writeFileSync(destFilePath, serializer.serializeToString(newDom), 'utf-8');
157
- }
158
- else {
159
- // remove from manifest
160
- // eslint-disable-next-line @typescript-eslint/no-shadow
161
- const profileName = xmlFile.replace(/(.*)(\.profile)/, '$1');
162
- excludedProfiles.push(profileName);
163
- if (this.generateProfileInformation) {
164
- const profile = this.profiles.find(({ ProfileName }) => ProfileName === profileName);
165
- if (profile) {
166
- profile.setIsPackaged(false);
167
- }
168
- }
169
- try {
170
- fs.unlinkSync(destFilePath);
171
- }
172
- catch (err) {
173
- // It is normal for the file to not exist if the profile is in the workspace but not in the directory being packaged.
174
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
175
- if (err instanceof Error && 'code' in err && err.code !== 'ENOENT') {
176
- throw err;
177
- }
178
- }
179
- }
71
+ fs.writeFileSync(xmlFileLocation, (0, profileRewriter_1.profileObjectToString)(adjustedProfile), 'utf-8');
180
72
  }
181
- });
182
- return excludedProfiles;
73
+ })
74
+ .map(({ xmlFileLocation }) => xmlFileLocation.replace(/(.*)(\.profile)/, '$1')));
183
75
  }
184
76
  /**
185
77
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
@@ -188,97 +80,29 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
188
80
  * @param excludedDirectories Direcotires not to generate profiles for
189
81
  */
190
82
  filterAndGenerateProfilesForManifest(typesArr, excludedDirectories = []) {
191
- const profilePaths = this.findAllProfiles(excludedDirectories);
192
- // Filter all profiles
193
- const filteredTypesArr = (typesArr ?? []).filter((kvp) => kvp.name[0] !== 'Profile');
194
- if (profilePaths) {
195
- const members = [];
196
- profilePaths.forEach((profilePath) => {
197
- // profile metadata can present in any directory in the package structure
198
- const profileNameMatch = profilePath.match(/([^/]+)\.profile-meta.xml/);
199
- const profileName = profileNameMatch ? profileNameMatch[1] : null;
200
- if (profileName) {
201
- members.push(profileName);
202
- if (this.generateProfileInformation) {
203
- this.profiles.push(new ProfileInformation(profileName, profilePath, true, []));
204
- }
205
- }
206
- });
207
- if (members.length > 0) {
208
- filteredTypesArr.push({ name: ['Profile'], members });
209
- }
210
- }
211
- return filteredTypesArr;
212
- }
213
- getProfileInformation() {
214
- return this.profiles;
215
- }
216
- copyNodes(originalDom, parentElement, childElement, members, appendToNode, profileName) {
217
- let nodesAdded = false;
218
- const nodes = originalDom.getElementsByTagName(parentElement);
219
- if (!nodes) {
220
- return nodesAdded;
221
- }
222
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
223
- for (let i = 0; i < nodes.length; i++) {
224
- const name = nodes[i].getElementsByTagName(childElement)[0].childNodes[0].nodeValue;
225
- if (name) {
226
- if (members.includes(name)) {
227
- // appendChild will take the passed in node (newNode) and find the parent if it exists and then remove
228
- // the newNode from the parent. This causes issues with the way this is copying the nodes, so pass in a clone instead.
229
- const currentNode = nodes[i].cloneNode(true);
230
- appendToNode.appendChild(currentNode);
231
- nodesAdded = true;
232
- }
233
- else if (this.generateProfileInformation) {
234
- // Tell the user which profile setting has been removed from the package
235
- const profile = this.profiles.find(({ ProfileName }) => ProfileName === profileName);
236
- if (profile) {
237
- profile.appendRemovedSetting(name);
238
- }
239
- }
240
- }
241
- }
242
- return nodesAdded;
243
- }
244
- findAllProfiles(excludedDirectories = []) {
245
- return glob.sync(path.join(this.project.getPath(), '**', '*.profile-meta.xml'), {
246
- ignore: excludedDirectories.map((dir) => `**/${dir}/**`),
83
+ const profilePathsWithNames = getProfilesWithNamesAndPaths({
84
+ projectPath: this.project.getPath(),
85
+ excludedDirectories,
247
86
  });
87
+ // Filter all profiles, and add back the ones we found names for
88
+ return typesArr
89
+ .filter((kvp) => kvp.name !== 'Profile')
90
+ .concat([{ name: 'Profile', members: profilePathsWithNames.map((i) => i.name) }]);
248
91
  }
249
92
  }
250
93
  exports.PackageProfileApi = PackageProfileApi;
251
- class ProfileInformation {
252
- constructor(ProfileName, ProfilePath, IsPackaged, settingsRemoved) {
253
- this.ProfileName = ProfileName;
254
- this.ProfilePath = ProfilePath;
255
- this.IsPackaged = IsPackaged;
256
- this.settingsRemoved = settingsRemoved;
257
- }
258
- setIsPackaged(IsPackaged) {
259
- this.IsPackaged = IsPackaged;
260
- }
261
- appendRemovedSetting(setting) {
262
- this.settingsRemoved.push(setting);
263
- }
264
- logDebug() {
265
- let info = profileApiMessages.getMessage('addProfileToPackage', [this.ProfileName, this.ProfilePath]);
266
- this.settingsRemoved.forEach((setting) => {
267
- info += '\n\t' + profileApiMessages.getMessage('removeProfileSetting', [setting, this.ProfileName]);
268
- });
269
- if (!this.IsPackaged) {
270
- info += '\n\t' + profileApiMessages.getMessage('removeProfile', [this.ProfileName]);
271
- }
272
- info += '\n';
273
- return info;
274
- }
275
- logInfo() {
276
- if (this.IsPackaged) {
277
- return profileApiMessages.getMessage('addProfileToPackage', [this.ProfileName, this.ProfilePath]);
278
- }
279
- else {
280
- return profileApiMessages.getMessage('profileNotIncluded', [this.ProfileName]);
281
- }
282
- }
283
- }
94
+ const findAllProfiles = ({ projectPath, excludedDirectories = [], }) => glob.sync(path.join(projectPath, '**', '*.profile-meta.xml'), {
95
+ ignore: excludedDirectories.map((dir) => `**/${dir}/**`),
96
+ });
97
+ const isProfilePathWithName = (profilePathWithName) => typeof profilePathWithName.name === 'string';
98
+ const profilePathToName = (profilePath) => profilePath.match(/([^/]+)\.profile-meta.xml/)?.[1];
99
+ const getProfilesWithNamesAndPaths = ({ projectPath, excludedDirectories, }) => findAllProfiles({ projectPath, excludedDirectories })
100
+ .map((profilePath) => ({ profilePath, name: profilePathToName(profilePath) }))
101
+ .filter(isProfilePathWithName);
102
+ const getXmlFileLocation = (destPath, profilePath) => path.join(destPath, path.basename(profilePath).replace(/(.*)(-meta.xml)/, '$1'));
103
+ const getRemovedSettings = (originalProfile, adjustedProfile) => {
104
+ const originalProfileSettings = Object.keys(originalProfile);
105
+ const adjustedProfileSettings = new Set(Object.keys(adjustedProfile));
106
+ return originalProfileSettings.filter((setting) => !adjustedProfileSettings.has(setting));
107
+ };
284
108
  //# sourceMappingURL=packageProfileApi.js.map
@@ -1,5 +1,5 @@
1
1
  import { ConvertResult } from '@salesforce/source-deploy-retrieve';
2
- import { MDFolderForArtifactOptions, PackageVersionCreateOptions, PackageVersionCreateRequestResult } from '../interfaces';
2
+ import { MDFolderForArtifactOptions, PackageVersionCreateOptions, PackageVersionCreateRequestResult, PackageXml } from '../interfaces';
3
3
  export declare class PackageVersionCreate {
4
4
  private options;
5
5
  private apiVersionFromPackageXml;
@@ -66,3 +66,8 @@ export declare class MetadataResolver {
66
66
  */
67
67
  private convertMetadata;
68
68
  }
69
+ export declare const packageXmlStringToPackageXmlJson: (rawXml: string) => PackageXml;
70
+ /**
71
+ * Converts PackageXmlJson to a string representing the Xml
72
+ * */
73
+ export declare const packageXmlJsonToXmlString: (packageXmlJson: PackageXml) => string;
@@ -6,15 +6,15 @@
6
6
  * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.MetadataResolver = exports.PackageVersionCreate = void 0;
9
+ exports.packageXmlJsonToXmlString = exports.packageXmlStringToPackageXmlJson = exports.MetadataResolver = exports.PackageVersionCreate = void 0;
10
10
  const path = require("path");
11
11
  const os = require("os");
12
12
  const fs = require("fs");
13
13
  const core_1 = require("@salesforce/core");
14
14
  const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve");
15
15
  const scratchOrgSettingsGenerator_1 = require("@salesforce/core/lib/org/scratchOrgSettingsGenerator");
16
- const xml2js = require("xml2js");
17
16
  const kit_1 = require("@salesforce/kit");
17
+ const fast_xml_parser_1 = require("fast-xml-parser");
18
18
  const pkgUtils = require("../utils/packageUtils");
19
19
  const packageUtils_1 = require("../utils/packageUtils");
20
20
  const interfaces_1 = require("../interfaces");
@@ -240,8 +240,6 @@ class PackageVersionCreate {
240
240
  sourceDir: sourceBaseDir,
241
241
  sourceApiVersion: this.project?.getSfProjectJson()?.get('sourceApiVersion') ?? undefined,
242
242
  };
243
- // Stores any additional client side info that might be needed later on in the process
244
- const clientSideInfo = new Map();
245
243
  await fs.promises.mkdir(packageVersBlobDirectory, { recursive: true });
246
244
  const settingsGenerator = new scratchOrgSettingsGenerator_1.default({ asDirectory: true });
247
245
  let packageDescriptorJson = (0, kit_1.cloneJson)(this.packageObject);
@@ -305,7 +303,19 @@ class PackageVersionCreate {
305
303
  }
306
304
  packageDescriptorJson.ancestorId = ancestorId;
307
305
  await fs.promises.writeFile(path.join(packageVersBlobDirectory, DESCRIPTOR_FILE), JSON.stringify(packageDescriptorJson), 'utf-8');
308
- await this.cleanGeneratedPackage(packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, clientSideInfo, settingsGenerator);
306
+ await this.cleanGeneratedPackage({
307
+ packageVersMetadataFolder,
308
+ packageVersProfileFolder,
309
+ unpackagedMetadataFolder,
310
+ seedMetadataFolder,
311
+ metadataZipFile,
312
+ settingsZipFile,
313
+ packageVersBlobDirectory,
314
+ packageVersBlobZipFile,
315
+ unpackagedMetadataZipFile,
316
+ seedMetadataZipFile,
317
+ settingsGenerator,
318
+ });
309
319
  return this.createRequestObject(preserveFiles, packageVersTmpRoot, packageVersBlobZipFile);
310
320
  }
311
321
  verifyHasSource(componentSet) {
@@ -313,22 +323,22 @@ class PackageVersionCreate {
313
323
  throw messages.createError('noSourceInRootDirectory', [this.packageObject.path ?? '<unknown>']);
314
324
  }
315
325
  }
316
- async cleanGeneratedPackage(packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, clientSideInfo, settingsGenerator) {
326
+ async cleanGeneratedPackage({ packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, settingsGenerator, }) {
317
327
  // As part of the source convert process, the package.xml has been written into the tmp metadata directory.
318
328
  // The package.xml may need to be manipulated due to processing profiles in the workspace or additional
319
329
  // metadata exclusions. If necessary, read the existing package.xml and then re-write it.
320
330
  const currentPackageXml = await fs.promises.readFile(path.join(packageVersMetadataFolder, 'package.xml'), 'utf8');
321
331
  // convert to json
322
- const packageJson = (await xml2js.parseStringPromise(currentPackageXml));
323
- if (!packageJson?.Package) {
332
+ const packageXmlAsJson = (0, exports.packageXmlStringToPackageXmlJson)(currentPackageXml);
333
+ if (!packageXmlAsJson) {
324
334
  throw messages.createError('packageXmlDoesNotContainPackage');
325
335
  }
326
- if (!packageJson?.Package.types) {
336
+ if (!packageXmlAsJson?.types) {
327
337
  throw messages.createError('packageXmlDoesNotContainPackageTypes');
328
338
  }
329
339
  fs.mkdirSync(packageVersMetadataFolder, { recursive: true });
330
340
  fs.mkdirSync(packageVersProfileFolder, { recursive: true });
331
- this.apiVersionFromPackageXml = packageJson.Package.version;
341
+ this.apiVersionFromPackageXml = packageXmlAsJson.version;
332
342
  const sourceApiVersion = this.project?.getSfProjectJson()?.get('sourceApiVersion');
333
343
  const hasSeedMetadata = await this.metadataResolver.resolveMetadata(this.packageObject.seedMetadata?.path, seedMetadataFolder, 'seedMDDirectoryDoesNotExist', sourceApiVersion);
334
344
  let hasUnpackagedMetadata = false;
@@ -341,32 +351,16 @@ class PackageVersionCreate {
341
351
  .getPackageDirectories()
342
352
  .map((packageDir) => packageDir.unpackagedMetadata?.path)
343
353
  .filter((packageDirPath) => packageDirPath);
344
- const typesArr = this.options?.profileApi?.filterAndGenerateProfilesForManifest(packageJson.Package.types, profileExcludeDirs) ??
345
- packageJson.Package.types;
354
+ const typesArr = this.options?.profileApi?.filterAndGenerateProfilesForManifest(packageXmlAsJson.types, profileExcludeDirs) ??
355
+ packageXmlAsJson.types;
346
356
  // Next generate profiles and retrieve any profiles that were excluded because they had no matching nodes.
347
- const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, {
348
- Package: typesArr,
349
- }, profileExcludeDirs);
350
- if (excludedProfiles?.length) {
351
- const profileIdx = typesArr.findIndex((e) => e.name[0] === 'Profile');
352
- typesArr[profileIdx].members = typesArr[profileIdx].members.filter((e) => !excludedProfiles.includes(e));
353
- }
354
- packageJson.Package.types = typesArr;
355
- // Re-write the package.xml in case profiles have been added or removed
356
- const xmlBuilder = new xml2js.Builder({
357
- xmldec: { version: '1.0', encoding: 'UTF-8' },
358
- });
359
- const xml = xmlBuilder.buildObject(packageJson);
360
- // Log information about the profiles being packaged up
361
- const profiles = this.options?.profileApi?.getProfileInformation() ?? [];
362
- profiles.forEach((profile) => {
363
- if (this.logger.shouldLog(core_1.LoggerLevel.DEBUG)) {
364
- this.logger.debug(profile.logDebug());
365
- }
366
- else if (this.logger.shouldLog(core_1.LoggerLevel.INFO)) {
367
- this.logger.info(profile.logInfo());
368
- }
357
+ const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, typesArr, profileExcludeDirs);
358
+ packageXmlAsJson.types = typesArr.map((type) => {
359
+ if (type.name !== 'Profile')
360
+ return type;
361
+ return { ...type, members: type.members.filter((m) => !excludedProfiles?.includes(m)) };
369
362
  });
363
+ const xml = (0, exports.packageXmlJsonToXmlString)(packageXmlAsJson);
370
364
  await fs.promises.writeFile(path.join(packageVersMetadataFolder, 'package.xml'), xml, 'utf-8');
371
365
  // Zip the packageVersMetadataFolder folder and put the zip in {packageVersBlobDirectory}/package.zip
372
366
  await (0, packageUtils_1.zipDir)(packageVersMetadataFolder, metadataZipFile);
@@ -512,11 +506,9 @@ class PackageVersionCreate {
512
506
  return this.pkg.getType();
513
507
  }
514
508
  async resolveUserLicenses(includeUserLicenses) {
515
- const shouldGenerateProfileInformation = this.logger.shouldLog(core_1.LoggerLevel.INFO) || this.logger.shouldLog(core_1.LoggerLevel.DEBUG);
516
509
  return packageProfileApi_1.PackageProfileApi.create({
517
510
  project: this.project,
518
511
  includeUserLicenses,
519
- generateProfileInformation: shouldGenerateProfileInformation,
520
512
  });
521
513
  }
522
514
  async validateOptionsForPackageType() {
@@ -855,4 +847,39 @@ class MetadataResolver {
855
847
  }
856
848
  }
857
849
  exports.MetadataResolver = MetadataResolver;
850
+ const packageXmlStringToPackageXmlJson = (rawXml) => {
851
+ const parser = new fast_xml_parser_1.XMLParser({
852
+ ignoreAttributes: true,
853
+ parseTagValue: false,
854
+ parseAttributeValue: false,
855
+ cdataPropName: '__cdata',
856
+ ignoreDeclaration: true,
857
+ numberParseOptions: { leadingZeros: false, hex: false },
858
+ // make sure types and members is always an array
859
+ isArray: (name) => ['types', 'members'].includes(name),
860
+ });
861
+ return parser.parse(rawXml).Package;
862
+ };
863
+ exports.packageXmlStringToPackageXmlJson = packageXmlStringToPackageXmlJson;
864
+ /**
865
+ * Converts PackageXmlJson to a string representing the Xml
866
+ * */
867
+ const packageXmlJsonToXmlString = (packageXmlJson) => {
868
+ const builder = new fast_xml_parser_1.XMLBuilder({
869
+ format: true,
870
+ indentBy: ' ',
871
+ ignoreAttributes: false,
872
+ cdataPropName: '__cdata',
873
+ processEntities: false,
874
+ attributeNamePrefix: '@@@',
875
+ });
876
+ return String(builder.build({
877
+ '?xml': {
878
+ '@@@version': '1.0',
879
+ '@@@encoding': 'UTF-8',
880
+ },
881
+ Package: { ...packageXmlJson, '@@@xmlns': 'http://soap.sforce.com/2006/04/metadata' },
882
+ }));
883
+ };
884
+ exports.packageXmlJsonToXmlString = packageXmlJsonToXmlString;
858
885
  //# sourceMappingURL=packageVersionCreate.js.map
@@ -0,0 +1,37 @@
1
+ import { Profile } from 'jsforce/api/metadata';
2
+ import { PackageXml } from '../interfaces';
3
+ type ProfileCustomSettingAccess = {
4
+ name: string;
5
+ enabled: boolean;
6
+ };
7
+ export type CorrectedProfile = Profile & {
8
+ customSettingAccesses: ProfileCustomSettingAccess[];
9
+ };
10
+ /**
11
+ *
12
+ * Takes a Profile that's been converted from package.xml to json.
13
+ * Filters out all Profile props that are not
14
+ * 1. used by packaging (ex: ipRanges)
15
+ * 2. present in the package.xml (ex: ClassAccesses for a class not in the package)
16
+ * 3. optionally retains the UserLicense prop only if the param is true
17
+ *
18
+ * @param profileJson json representation of a profile
19
+ * @param packageXml package.xml as json
20
+ * @param retainUserLicense boolean will preserve userLicense if true
21
+ * @returns Profile
22
+ */
23
+ export declare const profileRewriter: (profileJson: CorrectedProfile, packageXml: PackageMap, retainUserLicense?: boolean) => CorrectedProfile;
24
+ export declare const fieldCorrections: (fieldName: string) => string;
25
+ /**
26
+ * @param profileString: raw xml read from the file
27
+ * @returns CorrectedProfile (json representation of the profile)
28
+ */
29
+ export declare const profileStringToProfile: (profileString: string) => CorrectedProfile;
30
+ /** pass in an object that has the Profile props at the top level.
31
+ * This function will add the outer wrapper `Profile` and convert the result to xml
32
+ * */
33
+ export declare const profileObjectToString: (profileObject: Partial<CorrectedProfile>) => string;
34
+ /** it's easier to do lookups by Metadata Type on a Map */
35
+ export declare const manifestTypesToMap: (original: PackageXml['types']) => PackageMap;
36
+ type PackageMap = Map<string, string[]>;
37
+ export {};
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.manifestTypesToMap = exports.profileObjectToString = exports.profileStringToProfile = exports.fieldCorrections = exports.profileRewriter = void 0;
4
+ const fast_xml_parser_1 = require("fast-xml-parser");
5
+ /**
6
+ *
7
+ * Takes a Profile that's been converted from package.xml to json.
8
+ * Filters out all Profile props that are not
9
+ * 1. used by packaging (ex: ipRanges)
10
+ * 2. present in the package.xml (ex: ClassAccesses for a class not in the package)
11
+ * 3. optionally retains the UserLicense prop only if the param is true
12
+ *
13
+ * @param profileJson json representation of a profile
14
+ * @param packageXml package.xml as json
15
+ * @param retainUserLicense boolean will preserve userLicense if true
16
+ * @returns Profile
17
+ */
18
+ const profileRewriter = (profileJson, packageXml, retainUserLicense = false) => ({
19
+ ...Object.fromEntries(Object.entries(profileJson)
20
+ // remove settings that are not used for packaging
21
+ .filter(isRewriteProp)
22
+ // @ts-expect-error the previous filter restricts us to only things that appear in filterFunctions
23
+ .map(([key, value]) => [key, filterFunctions[key]?.(value, packageXml)] ?? [])
24
+ // some profileSettings might now be empty Arrays if the package.xml didn't have those types, so remove the entire property
25
+ .filter(([, value]) => (Array.isArray(value) ? value.length : true))),
26
+ // this one prop is controlled by a param. Put it back the way it was if the param is true
27
+ ...(retainUserLicense && profileJson.userLicense ? { userLicense: profileJson.userLicense } : {}),
28
+ });
29
+ exports.profileRewriter = profileRewriter;
30
+ // it's both a filter and a typeguard to make sure props are represented in filterFunctions
31
+ const isRewriteProp = (prop) => rewriteProps.includes(prop[0]);
32
+ const rewriteProps = [
33
+ 'objectPermissions',
34
+ 'fieldPermissions',
35
+ 'layoutAssignments',
36
+ 'applicationVisibilities',
37
+ 'classAccesses',
38
+ 'externalDataSourceAccesses',
39
+ 'tabVisibilities',
40
+ 'pageAccesses',
41
+ 'customPermissions',
42
+ 'customMetadataTypeAccesses',
43
+ 'customSettingAccesses',
44
+ 'recordTypeVisibilities',
45
+ ];
46
+ const filterFunctions = {
47
+ objectPermissions: (props, packageXml) => props.filter((item) => packageXml.get('CustomObject')?.includes(item.object)),
48
+ fieldPermissions: (props, packageXml) => props.filter((item) => packageXml.get('CustomField')?.includes((0, exports.fieldCorrections)(item.field))),
49
+ layoutAssignments: (props, packageXml) => props.filter((item) => packageXml.get('Layout')?.includes(item.layout)),
50
+ tabVisibilities: (props, packageXml) => props.filter((item) => packageXml.get('CustomTab')?.includes(item.tab)),
51
+ applicationVisibilities: (props, packageXml) => props.filter((item) => packageXml.get('CustomApplication')?.includes(item.application)),
52
+ classAccesses: (props, packageXml) => props.filter((item) => packageXml.get('ApexClass')?.includes(item.apexClass)),
53
+ customPermissions: (props, packageXml) => props.filter((item) => packageXml.get('CustomPermission')?.includes(item.name)),
54
+ pageAccesses: (props, packageXml) => props.filter((item) => packageXml.get('ApexPage')?.includes(item.apexPage)),
55
+ externalDataSourceAccesses: (props, packageXml) => props.filter((item) => packageXml.get('ExternalDataSource')?.includes(item.externalDataSource)),
56
+ recordTypeVisibilities: (props, packageXml) => props.filter((item) => packageXml.get('RecordType')?.includes(item.recordType)),
57
+ customSettingAccesses: (props, packageXml) => props.filter((item) => allMembers(packageXml).includes(item.name)),
58
+ customMetadataTypeAccesses: (props, packageXml) => props.filter((item) => allMembers(packageXml).includes(item.name)),
59
+ };
60
+ const allMembers = (packageXml) => Array.from(packageXml.values()).flat();
61
+ // github.com/forcedotcom/cli/issues/2278
62
+ // Activity Object is polymorphic (Task and Event)
63
+ // package.xml will display them as 'Activity'
64
+ // profile.fieldPermissions will display them with the more specific 'Task' or 'Event'
65
+ const fieldCorrections = (fieldName) => fieldName.replace(/^Event\./, 'Activity.').replace(/^Task\./, 'Activity.');
66
+ exports.fieldCorrections = fieldCorrections;
67
+ /**
68
+ * @param profileString: raw xml read from the file
69
+ * @returns CorrectedProfile (json representation of the profile)
70
+ */
71
+ const profileStringToProfile = (profileString) => {
72
+ const parser = new fast_xml_parser_1.XMLParser({
73
+ ignoreAttributes: true,
74
+ parseTagValue: false,
75
+ parseAttributeValue: false,
76
+ cdataPropName: '__cdata',
77
+ ignoreDeclaration: true,
78
+ numberParseOptions: { leadingZeros: false, hex: false },
79
+ isArray: (name) => rewriteProps.includes(name),
80
+ });
81
+ return parser.parse(profileString).Profile;
82
+ };
83
+ exports.profileStringToProfile = profileStringToProfile;
84
+ /** pass in an object that has the Profile props at the top level.
85
+ * This function will add the outer wrapper `Profile` and convert the result to xml
86
+ * */
87
+ const profileObjectToString = (profileObject) => {
88
+ const builder = new fast_xml_parser_1.XMLBuilder({
89
+ format: true,
90
+ indentBy: ' ',
91
+ ignoreAttributes: false,
92
+ cdataPropName: '__cdata',
93
+ processEntities: false,
94
+ attributeNamePrefix: '@@@',
95
+ });
96
+ return String(builder.build({
97
+ '?xml': {
98
+ '@@@version': '1.0',
99
+ '@@@encoding': 'UTF-8',
100
+ },
101
+ Profile: { ...profileObject, '@@@xmlns': 'http://soap.sforce.com/2006/04/metadata' },
102
+ }));
103
+ };
104
+ exports.profileObjectToString = profileObjectToString;
105
+ /** it's easier to do lookups by Metadata Type on a Map */
106
+ const manifestTypesToMap = (original) => new Map(original.map((item) => [item.name, item.members]));
107
+ exports.manifestTypesToMap = manifestTypesToMap;
108
+ //# sourceMappingURL=profileRewriter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/packaging",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Packaging library for the Salesforce packaging platform",
5
5
  "main": "lib/exported",
6
6
  "types": "lib/exported.d.ts",
@@ -42,22 +42,18 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "@oclif/core": "^2.9.4",
45
- "@salesforce/core": "^4.3.2",
46
- "@salesforce/kit": "^3.0.3",
45
+ "@salesforce/core": "^4.3.10",
46
+ "@salesforce/kit": "^3.0.5",
47
47
  "@salesforce/schemas": "^1.6.0",
48
- "@salesforce/source-deploy-retrieve": "^8.6.0",
48
+ "@salesforce/source-deploy-retrieve": "^9.3.4",
49
49
  "@salesforce/ts-types": "^2.0.3",
50
- "@xmldom/xmldom": "^0.8.9",
51
- "debug": "^4.3.4",
50
+ "fast-xml-parser": "^4.2.5",
52
51
  "globby": "^11",
53
52
  "graphology": "^0.25.1",
54
53
  "graphology-traversal": "^0.3.1",
55
54
  "graphology-types": "^0.24.7",
56
- "js2xmlparser": "^4.0.2",
57
- "jsforce": "^2.0.0-beta.23",
58
- "jszip": "^3.10.1",
59
- "ts-retry-promise": "^0.7.0",
60
- "xml2js": "^0.6.0"
55
+ "jsforce": "^2.0.0-beta.27",
56
+ "jszip": "^3.10.1"
61
57
  },
62
58
  "devDependencies": {
63
59
  "@salesforce/cli-plugins-testkit": "^4.1.1",
@@ -65,14 +61,11 @@
65
61
  "@salesforce/dev-scripts": "^5.4.3",
66
62
  "@salesforce/prettier-config": "^0.0.3",
67
63
  "@salesforce/ts-sinon": "^1.4.12",
68
- "@types/debug": "4.1.8",
69
64
  "@types/globby": "^9.1.0",
70
65
  "@types/jszip": "^3.4.1",
71
- "@types/xml2js": "^0.4.11",
72
66
  "@typescript-eslint/eslint-plugin": "^5.60.0",
73
67
  "@typescript-eslint/parser": "^5.61.0",
74
68
  "chai": "^4.3.7",
75
- "commitizen": "^4.2.6",
76
69
  "eslint": "^8.44.0",
77
70
  "eslint-config-prettier": "^8.8.0",
78
71
  "eslint-config-salesforce": "^2.0.1",