@salesforce/packaging 2.2.7 → 2.2.9-profiles.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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';
@@ -269,7 +270,6 @@ export type PackageVersionCreateRequestQueryOptions = {
269
270
  export type ProfileApiOptions = {
270
271
  project: SfProject;
271
272
  includeUserLicenses: boolean;
272
- generateProfileInformation: boolean;
273
273
  };
274
274
  export type PackageVersionReportResult = Partial<Omit<PackagingSObjects.Package2Version, 'AncestorId' | 'HasPassedCodeCoverageCheck' | 'HasMetadataRemoved'>> & {
275
275
  Package2: Partial<Omit<PackagingSObjects.Package2, 'IsOrgDependent'>> & {
@@ -370,3 +370,6 @@ export declare const Package1VersionEvents: {
370
370
  progress: string;
371
371
  };
372
372
  };
373
+ export type PackageXml = {
374
+ Package: Pick<Package, 'types' | 'version'>;
375
+ };
@@ -1,21 +1,9 @@
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
  /**
@@ -23,41 +11,15 @@ export declare class PackageProfileApi extends AsyncCreatable<ProfileApiOptions>
23
11
  * to items in the manifest.
24
12
  *
25
13
  * @param destPath location of new profiles
26
- * @param manifest
14
+ * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
27
15
  * @param excludedDirectories Directories to not include profiles from
28
16
  */
29
- generateProfiles(destPath: string, manifest: {
30
- Package: Array<{
31
- name: string[];
32
- members: string[];
33
- }>;
34
- }, excludedDirectories?: string[]): string[];
17
+ generateProfiles(destPath: string, manifestTypes: PackageXml['Package']['types'], excludedDirectories?: string[]): string[];
35
18
  /**
36
19
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
37
20
  *
38
21
  * @param typesArr array of objects { name[], members[] } that represent package types JSON.
39
22
  * @param excludedDirectories Direcotires not to generate profiles for
40
23
  */
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;
24
+ filterAndGenerateProfilesForManifest(typesArr: PackageXml['Package']['types'], excludedDirectories?: string[]): PackageXml['Package']['types'];
51
25
  }
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;
62
- }
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() { }
@@ -96,90 +34,38 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
96
34
  * to items in the manifest.
97
35
  *
98
36
  * @param destPath location of new profiles
99
- * @param manifest
37
+ * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
100
38
  * @param excludedDirectories Directories to not include profiles from
101
39
  */
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
- }
139
- });
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
- }
40
+ generateProfiles(destPath, manifestTypes, excludedDirectories = []) {
41
+ const logger = core_1.Logger.childFromRoot('PackageProfileApi');
42
+ const profilePathsWithNames = getProfilesWithNamesAndPaths({
43
+ projectPath: this.project.getPath(),
44
+ excludedDirectories,
45
+ });
46
+ const results = profilePathsWithNames.map(({ profilePath, name: profileName }) => {
47
+ const originalProfile = (0, profileRewriter_1.profileStringToProfile)(fs.readFileSync(profilePath, 'utf-8'));
48
+ const adjustedProfile = (0, profileRewriter_1.profileRewriter)(originalProfile, (0, profileRewriter_1.manifestTypesToMap)(manifestTypes), this.includeUserLicenses);
49
+ const hasContent = Object.keys(adjustedProfile).length;
50
+ return { profileName, profilePath, hasContent, adjustedProfile, originalProfile };
51
+ });
52
+ results.map(({ profilePath, adjustedProfile, hasContent, profileName, originalProfile }) => {
53
+ if (!hasContent) {
54
+ 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.`);
180
55
  }
56
+ logger.info(profileApiMessages.getMessage('addProfileToPackage', [profileName, profilePath]));
57
+ getRemovedSettings(originalProfile, adjustedProfile).forEach((setting) => {
58
+ logger.info(profileApiMessages.getMessage('removeProfileSetting', [setting, profileName]));
59
+ });
60
+ fs.writeFileSync(getXmlFileLocation(destPath, profilePath), (0, profileRewriter_1.profileObjectToString)(adjustedProfile), 'utf-8');
61
+ });
62
+ return results.map((profile) => {
63
+ const xmlFile = getXmlFileLocation(destPath, profile.profilePath);
64
+ const replacedProfileName = xmlFile.replace(/(.*)(\.profile)/, '$1');
65
+ deleteButAllowEnoent(xmlFile);
66
+ logger.info(profileApiMessages.getMessage('profileNotIncluded', [replacedProfileName]));
67
+ return replacedProfileName;
181
68
  });
182
- return excludedProfiles;
183
69
  }
184
70
  /**
185
71
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
@@ -188,97 +74,40 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
188
74
  * @param excludedDirectories Direcotires not to generate profiles for
189
75
  */
190
76
  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}/**`),
77
+ const profilePathsWithNames = getProfilesWithNamesAndPaths({
78
+ projectPath: this.project.getPath(),
79
+ excludedDirectories,
247
80
  });
81
+ // Filter all profiles, and add back the ones we found names for
82
+ return typesArr
83
+ .filter((kvp) => kvp.name !== 'Profile')
84
+ .concat([{ name: 'Profile', members: profilePathsWithNames.map((i) => i.name) }]);
248
85
  }
249
86
  }
250
87
  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);
88
+ const findAllProfiles = ({ projectPath, excludedDirectories = [], }) => glob.sync(path.join(projectPath, '**', '*.profile-meta.xml'), {
89
+ ignore: excludedDirectories.map((dir) => `**/${dir}/**`),
90
+ });
91
+ const isProfilePathWithName = (profilePathWithName) => typeof profilePathWithName.name === 'string';
92
+ const profilePathToName = (profilePath) => profilePath.match(/([^/]+)\.profile-meta.xml/)?.[1];
93
+ const getProfilesWithNamesAndPaths = ({ projectPath, excludedDirectories, }) => findAllProfiles({ projectPath, excludedDirectories })
94
+ .map((profilePath) => ({ profilePath, name: profilePathToName(profilePath) }))
95
+ .filter(isProfilePathWithName);
96
+ const getXmlFileLocation = (destPath, profilePath) => path.join(destPath, path.basename(profilePath).replace(/(.*)(-meta.xml)/, '$1'));
97
+ const deleteButAllowEnoent = (destFilePath) => {
98
+ try {
99
+ fs.unlinkSync(destFilePath);
263
100
  }
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]);
101
+ catch (err) {
102
+ // It is normal for the file to not exist if the profile is in the workspace but not in the directory being packaged.
103
+ if (err instanceof Error && 'code' in err && err.code !== 'ENOENT') {
104
+ throw err;
281
105
  }
282
106
  }
283
- }
107
+ };
108
+ const getRemovedSettings = (originalProfile, adjustedProfile) => {
109
+ const originalProfileSettings = Object.keys(originalProfile);
110
+ const adjustedProfileSettings = Object.keys(adjustedProfile);
111
+ return originalProfileSettings.filter((setting) => !adjustedProfileSettings.includes(setting));
112
+ };
284
113
  //# sourceMappingURL=packageProfileApi.js.map
@@ -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 = (await xml2js.parseStringPromise(currentPackageXml));
333
+ if (!packageXmlAsJson?.Package) {
324
334
  throw messages.createError('packageXmlDoesNotContainPackage');
325
335
  }
326
- if (!packageJson?.Package.types) {
336
+ if (!packageXmlAsJson?.Package?.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.Package.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,19 @@ 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.Package.types, profileExcludeDirs) ?? packageXmlAsJson.Package.types;
346
355
  // 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);
356
+ const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, typesArr, profileExcludeDirs);
350
357
  if (excludedProfiles?.length) {
351
- const profileIdx = typesArr.findIndex((e) => e.name[0] === 'Profile');
358
+ const profileIdx = typesArr.findIndex((e) => e.name === 'Profile');
352
359
  typesArr[profileIdx].members = typesArr[profileIdx].members.filter((e) => !excludedProfiles.includes(e));
353
360
  }
354
- packageJson.Package.types = typesArr;
361
+ packageXmlAsJson.Package.types = typesArr;
355
362
  // Re-write the package.xml in case profiles have been added or removed
356
363
  const xmlBuilder = new xml2js.Builder({
357
364
  xmldec: { version: '1.0', encoding: 'UTF-8' },
358
365
  });
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
- }
369
- });
366
+ const xml = xmlBuilder.buildObject(packageXmlAsJson);
370
367
  await fs.promises.writeFile(path.join(packageVersMetadataFolder, 'package.xml'), xml, 'utf-8');
371
368
  // Zip the packageVersMetadataFolder folder and put the zip in {packageVersBlobDirectory}/package.zip
372
369
  await (0, packageUtils_1.zipDir)(packageVersMetadataFolder, metadataZipFile);
@@ -512,11 +509,9 @@ class PackageVersionCreate {
512
509
  return this.pkg.getType();
513
510
  }
514
511
  async resolveUserLicenses(includeUserLicenses) {
515
- const shouldGenerateProfileInformation = this.logger.shouldLog(core_1.LoggerLevel.INFO) || this.logger.shouldLog(core_1.LoggerLevel.DEBUG);
516
512
  return packageProfileApi_1.PackageProfileApi.create({
517
513
  project: this.project,
518
514
  includeUserLicenses,
519
- generateProfileInformation: shouldGenerateProfileInformation,
520
515
  });
521
516
  }
522
517
  async validateOptionsForPackageType() {
@@ -0,0 +1,31 @@
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
+ export declare const profileStringToProfile: (profileString: string) => CorrectedProfile;
26
+ /** pass in an object that has the Profile props at the top level. This function will add the outer wrapper `Profile` */
27
+ export declare const profileObjectToString: (profileObject: Partial<CorrectedProfile>) => string;
28
+ /** it's easier to do lookups by Metadata Type on a Map */
29
+ export declare const manifestTypesToMap: (original: PackageXml['Package']['types']) => PackageMap;
30
+ type PackageMap = Map<string, string[]>;
31
+ export {};
@@ -0,0 +1,101 @@
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('Application')?.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
+ const profileStringToProfile = (profileString) => {
68
+ const parser = new fast_xml_parser_1.XMLParser({
69
+ ignoreAttributes: false,
70
+ parseTagValue: false,
71
+ parseAttributeValue: false,
72
+ cdataPropName: '__cdata',
73
+ ignoreDeclaration: true,
74
+ numberParseOptions: { leadingZeros: false, hex: false },
75
+ });
76
+ return parser.parse(profileString);
77
+ };
78
+ exports.profileStringToProfile = profileStringToProfile;
79
+ /** pass in an object that has the Profile props at the top level. This function will add the outer wrapper `Profile` */
80
+ const profileObjectToString = (profileObject) => {
81
+ const builder = new fast_xml_parser_1.XMLBuilder({
82
+ format: true,
83
+ indentBy: ' ',
84
+ ignoreAttributes: false,
85
+ cdataPropName: '__cdata',
86
+ processEntities: false,
87
+ attributeNamePrefix: '@@@',
88
+ });
89
+ return String(builder.build({
90
+ '?xml': {
91
+ '@@@version': '1.0',
92
+ '@@@encoding': 'UTF-8',
93
+ },
94
+ Profile: { ...profileObject, '@@@xmlns': 'http://soap.sforce.com/2006/04/metadata' },
95
+ }));
96
+ };
97
+ exports.profileObjectToString = profileObjectToString;
98
+ /** it's easier to do lookups by Metadata Type on a Map */
99
+ const manifestTypesToMap = (original) => new Map(original.map((item) => [item.name, item.members]));
100
+ exports.manifestTypesToMap = manifestTypesToMap;
101
+ //# sourceMappingURL=profileRewriter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/packaging",
3
- "version": "2.2.7",
3
+ "version": "2.2.9-profiles.0",
4
4
  "description": "Packaging library for the Salesforce packaging platform",
5
5
  "main": "lib/exported",
6
6
  "types": "lib/exported.d.ts",
@@ -42,21 +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",
55
+ "jsforce": "^2.0.0-beta.27",
58
56
  "jszip": "^3.10.1",
59
- "ts-retry-promise": "^0.7.0",
60
57
  "xml2js": "^0.6.0"
61
58
  },
62
59
  "devDependencies": {
@@ -65,14 +62,12 @@
65
62
  "@salesforce/dev-scripts": "^5.4.3",
66
63
  "@salesforce/prettier-config": "^0.0.3",
67
64
  "@salesforce/ts-sinon": "^1.4.11",
68
- "@types/debug": "4.1.8",
69
65
  "@types/globby": "^9.1.0",
70
66
  "@types/jszip": "^3.4.1",
71
67
  "@types/xml2js": "^0.4.11",
72
68
  "@typescript-eslint/eslint-plugin": "^5.60.0",
73
69
  "@typescript-eslint/parser": "^5.61.0",
74
70
  "chai": "^4.3.7",
75
- "commitizen": "^4.2.6",
76
71
  "eslint": "^8.44.0",
77
72
  "eslint-config-prettier": "^8.8.0",
78
73
  "eslint-config-salesforce": "^2.0.1",