@salesforce/packaging 2.2.9-profiles.0 → 2.3.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,7 +4,6 @@ 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';
8
7
  import { PackageProfileApi } from '../package/packageProfileApi';
9
8
  import { PackageAncestryNode } from '../package/packageAncestry';
10
9
  import { PackagingSObjects } from './packagingSObjects';
@@ -49,6 +48,7 @@ export type PackageVersionCreateRequestResult = {
49
48
  CreatedDate: string;
50
49
  HasMetadataRemoved: boolean | null;
51
50
  CreatedBy: string;
51
+ ConvertedFromVersionId: string | null;
52
52
  };
53
53
  export declare const PackageVersionCreateRequestResultInProgressStatuses: PackagingSObjects.Package2VersionStatus[];
54
54
  export type PackageVersionCreateRequestError = {
@@ -155,6 +155,7 @@ export type PackageVersionListOptions = {
155
155
  verbose?: boolean;
156
156
  concise?: boolean;
157
157
  isReleased?: boolean;
158
+ showConversionsOnly?: boolean;
158
159
  };
159
160
  export type PackageVersionUpdateOptions = {
160
161
  InstallKey?: string;
@@ -266,10 +267,12 @@ export type PackageVersionCreateRequestQueryOptions = {
266
267
  createdlastdays?: number;
267
268
  status?: 'Queued' | 'InProgress' | 'Success' | 'Error';
268
269
  id?: string;
270
+ showConversionsOnly?: boolean;
269
271
  };
270
272
  export type ProfileApiOptions = {
271
273
  project: SfProject;
272
274
  includeUserLicenses: boolean;
275
+ generateProfileInformation: boolean;
273
276
  };
274
277
  export type PackageVersionReportResult = Partial<Omit<PackagingSObjects.Package2Version, 'AncestorId' | 'HasPassedCodeCoverageCheck' | 'HasMetadataRemoved'>> & {
275
278
  Package2: Partial<Omit<PackagingSObjects.Package2, 'IsOrgDependent'>> & {
@@ -370,6 +373,3 @@ export declare const Package1VersionEvents: {
370
373
  progress: string;
371
374
  };
372
375
  };
373
- export type PackageXml = {
374
- Package: Pick<Package, 'types' | 'version'>;
375
- };
@@ -1,9 +1,21 @@
1
1
  import { SfProject } from '@salesforce/core';
2
2
  import { AsyncCreatable } from '@salesforce/kit';
3
- import { PackageXml, ProfileApiOptions } from '../interfaces';
3
+ import { 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
+ };
5
16
  project: SfProject;
6
17
  includeUserLicenses: boolean;
18
+ generateProfileInformation: boolean;
7
19
  constructor(options: ProfileApiOptions);
8
20
  init(): Promise<void>;
9
21
  /**
@@ -11,15 +23,41 @@ export declare class PackageProfileApi extends AsyncCreatable<ProfileApiOptions>
11
23
  * to items in the manifest.
12
24
  *
13
25
  * @param destPath location of new profiles
14
- * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
26
+ * @param manifest
15
27
  * @param excludedDirectories Directories to not include profiles from
16
28
  */
17
- generateProfiles(destPath: string, manifestTypes: PackageXml['Package']['types'], excludedDirectories?: string[]): string[];
29
+ generateProfiles(destPath: string, manifest: {
30
+ Package: Array<{
31
+ name: string[];
32
+ members: string[];
33
+ }>;
34
+ }, excludedDirectories?: string[]): string[];
18
35
  /**
19
36
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
20
37
  *
21
38
  * @param typesArr array of objects { name[], members[] } that represent package types JSON.
22
39
  * @param excludedDirectories Direcotires not to generate profiles for
23
40
  */
24
- filterAndGenerateProfilesForManifest(typesArr: PackageXml['Package']['types'], excludedDirectories?: string[]): PackageXml['Package']['types'];
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;
25
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;
62
+ }
63
+ export {};
@@ -1,20 +1,77 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PackageProfileApi = void 0;
4
2
  /*
5
3
  * Copyright (c) 2022, salesforce.com, inc.
6
4
  * All rights reserved.
7
5
  * Licensed under the BSD 3-Clause license.
8
6
  * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
9
7
  */
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");
13
14
  const core_1 = require("@salesforce/core");
14
15
  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
+ };
18
75
  /*
19
76
  * This class provides functions used to re-write .profiles in the workspace when creating a package2 version.
20
77
  * All profiles found in the workspaces are extracted out and then re-written to only include metadata in the profile
@@ -23,9 +80,14 @@ const profileApiMessages = core_1.Messages.loadMessages('@salesforce/packaging',
23
80
  class PackageProfileApi extends kit_1.AsyncCreatable {
24
81
  constructor(options) {
25
82
  super(options);
83
+ this.profiles = [];
84
+ this.nodeEntities = NODE_ENTITIES;
85
+ this.otherProfileSettings = OTHER_PROFILE_SETTINGS;
26
86
  this.includeUserLicenses = false;
87
+ this.generateProfileInformation = false;
27
88
  this.project = options.project;
28
89
  this.includeUserLicenses = options.includeUserLicenses ?? false;
90
+ this.generateProfileInformation = options.generateProfileInformation ?? false;
29
91
  }
30
92
  // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function
31
93
  async init() { }
@@ -34,38 +96,90 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
34
96
  * to items in the manifest.
35
97
  *
36
98
  * @param destPath location of new profiles
37
- * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types
99
+ * @param manifest
38
100
  * @param excludedDirectories Directories to not include profiles from
39
101
  */
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.`);
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
+ }
55
180
  }
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;
68
181
  });
182
+ return excludedProfiles;
69
183
  }
70
184
  /**
71
185
  * Filter out all profiles in the manifest and if any profiles exists in the workspace, add them to the manifest.
@@ -74,40 +188,97 @@ class PackageProfileApi extends kit_1.AsyncCreatable {
74
188
  * @param excludedDirectories Direcotires not to generate profiles for
75
189
  */
76
190
  filterAndGenerateProfilesForManifest(typesArr, excludedDirectories = []) {
77
- const profilePathsWithNames = getProfilesWithNamesAndPaths({
78
- projectPath: this.project.getPath(),
79
- 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}/**`),
80
247
  });
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) }]);
85
248
  }
86
249
  }
87
250
  exports.PackageProfileApi = PackageProfileApi;
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);
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);
100
263
  }
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;
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]);
105
271
  }
272
+ info += '\n';
273
+ return info;
106
274
  }
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
- };
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
+ }
113
284
  //# sourceMappingURL=packageProfileApi.js.map
@@ -240,6 +240,8 @@ 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();
243
245
  await fs.promises.mkdir(packageVersBlobDirectory, { recursive: true });
244
246
  const settingsGenerator = new scratchOrgSettingsGenerator_1.default({ asDirectory: true });
245
247
  let packageDescriptorJson = (0, kit_1.cloneJson)(this.packageObject);
@@ -303,19 +305,7 @@ class PackageVersionCreate {
303
305
  }
304
306
  packageDescriptorJson.ancestorId = ancestorId;
305
307
  await fs.promises.writeFile(path.join(packageVersBlobDirectory, DESCRIPTOR_FILE), JSON.stringify(packageDescriptorJson), 'utf-8');
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
- });
308
+ await this.cleanGeneratedPackage(packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, clientSideInfo, settingsGenerator);
319
309
  return this.createRequestObject(preserveFiles, packageVersTmpRoot, packageVersBlobZipFile);
320
310
  }
321
311
  verifyHasSource(componentSet) {
@@ -323,22 +313,22 @@ class PackageVersionCreate {
323
313
  throw messages.createError('noSourceInRootDirectory', [this.packageObject.path ?? '<unknown>']);
324
314
  }
325
315
  }
326
- async cleanGeneratedPackage({ packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, settingsGenerator, }) {
316
+ async cleanGeneratedPackage(packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, clientSideInfo, settingsGenerator) {
327
317
  // As part of the source convert process, the package.xml has been written into the tmp metadata directory.
328
318
  // The package.xml may need to be manipulated due to processing profiles in the workspace or additional
329
319
  // metadata exclusions. If necessary, read the existing package.xml and then re-write it.
330
320
  const currentPackageXml = await fs.promises.readFile(path.join(packageVersMetadataFolder, 'package.xml'), 'utf8');
331
321
  // convert to json
332
- const packageXmlAsJson = (await xml2js.parseStringPromise(currentPackageXml));
333
- if (!packageXmlAsJson?.Package) {
322
+ const packageJson = (await xml2js.parseStringPromise(currentPackageXml));
323
+ if (!packageJson?.Package) {
334
324
  throw messages.createError('packageXmlDoesNotContainPackage');
335
325
  }
336
- if (!packageXmlAsJson?.Package?.types) {
326
+ if (!packageJson?.Package.types) {
337
327
  throw messages.createError('packageXmlDoesNotContainPackageTypes');
338
328
  }
339
329
  fs.mkdirSync(packageVersMetadataFolder, { recursive: true });
340
330
  fs.mkdirSync(packageVersProfileFolder, { recursive: true });
341
- this.apiVersionFromPackageXml = packageXmlAsJson.Package.version;
331
+ this.apiVersionFromPackageXml = packageJson.Package.version;
342
332
  const sourceApiVersion = this.project?.getSfProjectJson()?.get('sourceApiVersion');
343
333
  const hasSeedMetadata = await this.metadataResolver.resolveMetadata(this.packageObject.seedMetadata?.path, seedMetadataFolder, 'seedMDDirectoryDoesNotExist', sourceApiVersion);
344
334
  let hasUnpackagedMetadata = false;
@@ -351,19 +341,32 @@ class PackageVersionCreate {
351
341
  .getPackageDirectories()
352
342
  .map((packageDir) => packageDir.unpackagedMetadata?.path)
353
343
  .filter((packageDirPath) => packageDirPath);
354
- const typesArr = this.options?.profileApi?.filterAndGenerateProfilesForManifest(packageXmlAsJson.Package.types, profileExcludeDirs) ?? packageXmlAsJson.Package.types;
344
+ const typesArr = this.options?.profileApi?.filterAndGenerateProfilesForManifest(packageJson.Package.types, profileExcludeDirs) ??
345
+ packageJson.Package.types;
355
346
  // Next generate profiles and retrieve any profiles that were excluded because they had no matching nodes.
356
- const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, typesArr, profileExcludeDirs);
347
+ const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, {
348
+ Package: typesArr,
349
+ }, profileExcludeDirs);
357
350
  if (excludedProfiles?.length) {
358
- const profileIdx = typesArr.findIndex((e) => e.name === 'Profile');
351
+ const profileIdx = typesArr.findIndex((e) => e.name[0] === 'Profile');
359
352
  typesArr[profileIdx].members = typesArr[profileIdx].members.filter((e) => !excludedProfiles.includes(e));
360
353
  }
361
- packageXmlAsJson.Package.types = typesArr;
354
+ packageJson.Package.types = typesArr;
362
355
  // Re-write the package.xml in case profiles have been added or removed
363
356
  const xmlBuilder = new xml2js.Builder({
364
357
  xmldec: { version: '1.0', encoding: 'UTF-8' },
365
358
  });
366
- const xml = xmlBuilder.buildObject(packageXmlAsJson);
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
+ });
367
370
  await fs.promises.writeFile(path.join(packageVersMetadataFolder, 'package.xml'), xml, 'utf-8');
368
371
  // Zip the packageVersMetadataFolder folder and put the zip in {packageVersBlobDirectory}/package.zip
369
372
  await (0, packageUtils_1.zipDir)(packageVersMetadataFolder, metadataZipFile);
@@ -509,9 +512,11 @@ class PackageVersionCreate {
509
512
  return this.pkg.getType();
510
513
  }
511
514
  async resolveUserLicenses(includeUserLicenses) {
515
+ const shouldGenerateProfileInformation = this.logger.shouldLog(core_1.LoggerLevel.INFO) || this.logger.shouldLog(core_1.LoggerLevel.DEBUG);
512
516
  return packageProfileApi_1.PackageProfileApi.create({
513
517
  project: this.project,
514
518
  includeUserLicenses,
519
+ generateProfileInformation: shouldGenerateProfileInformation,
515
520
  });
516
521
  }
517
522
  async validateOptionsForPackageType() {
@@ -14,7 +14,7 @@ core_1.Messages.importMessagesDirectory(__dirname);
14
14
  const messages = core_1.Messages.loadMessages('@salesforce/packaging', 'package_version_create');
15
15
  const STATUS_ERROR = 'Error';
16
16
  const QUERY = 'SELECT Id, Status, Package2Id, Package2VersionId, Package2Version.SubscriberPackageVersionId, Tag, Branch, ' +
17
- 'CreatedDate, Package2Version.HasMetadataRemoved, CreatedById ' +
17
+ 'CreatedDate, Package2Version.HasMetadataRemoved, CreatedById, IsConversionRequest, Package2Version.ConvertedFromVersionId ' +
18
18
  'FROM Package2VersionCreateRequest ' +
19
19
  '%s' + // WHERE, if applicable
20
20
  'ORDER BY CreatedDate desc';
@@ -58,8 +58,23 @@ async function query(query, connection) {
58
58
  CreatedDate: formatDate(new Date(record.CreatedDate)),
59
59
  HasMetadataRemoved: record.Package2Version != null ? record.Package2Version.HasMetadataRemoved : null,
60
60
  CreatedBy: record.CreatedById,
61
+ ConvertedFromVersionId: convertedFromVersionMessage(record.Status, record.Package2Version?.ConvertedFromVersionId),
61
62
  }));
62
63
  }
64
+ function convertedFromVersionMessage(status, convertedFromVersionId) {
65
+ switch (status) {
66
+ case 'Success':
67
+ return convertedFromVersionId;
68
+ case 'Queued':
69
+ return messages.getMessage('IdUnavailableWhenQueued');
70
+ case 'InProgress':
71
+ return messages.getMessage('IdUnavailableWhenInProgress');
72
+ case 'Error':
73
+ return messages.getMessage('IdUnavailableWhenError');
74
+ default:
75
+ return messages.getMessage('IdUnavailableWhenInProgress');
76
+ }
77
+ }
63
78
  async function queryForErrors(packageVersionCreateRequestId, connection) {
64
79
  const queryResult = await connection.tooling.query(`SELECT Message FROM Package2VersionCreateRequestError WHERE ParentRequest.Id = '${packageVersionCreateRequestId}'`);
65
80
  return queryResult.records ? queryResult.records.map((record) => record.Message) : [];
@@ -80,6 +95,10 @@ function constructWhere(options) {
80
95
  if (options?.status) {
81
96
  where.push(`Status = '${options.status.toLowerCase()}'`);
82
97
  }
98
+ // show only conversions
99
+ if (options?.showConversionsOnly) {
100
+ where.push('IsConversionRequest = true ');
101
+ }
83
102
  return where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
84
103
  }
85
104
  //# sourceMappingURL=packageVersionCreateRequest.js.map
@@ -34,11 +34,11 @@ const defaultFields = [
34
34
  'AncestorId',
35
35
  'ValidationSkipped',
36
36
  'CreatedById',
37
+ 'ConvertedFromVersionId',
37
38
  ];
38
39
  const verboseFields = [
39
40
  'CodeCoverage',
40
41
  'HasPassedCodeCoverageCheck',
41
- 'ConvertedFromVersionId',
42
42
  'ReleaseVersion',
43
43
  'BuildDurationInSeconds',
44
44
  'HasMetadataRemoved',
@@ -107,6 +107,9 @@ function constructWhere(options) {
107
107
  if (options?.isReleased) {
108
108
  where.push('IsReleased = true');
109
109
  }
110
+ if (options?.showConversionsOnly) {
111
+ where.push('ConvertedFromVersionId != null');
112
+ }
110
113
  // exclude deleted
111
114
  where.push('IsDeprecated = false');
112
115
  return where;
@@ -14,7 +14,7 @@ const core_1 = require("@salesforce/core");
14
14
  const pkgUtils = require("../utils/packageUtils");
15
15
  const QUERY = 'SELECT Package2Id, SubscriberPackageVersionId, Name, Description, Tag, Branch, AncestorId, ValidationSkipped, ' +
16
16
  'MajorVersion, MinorVersion, PatchVersion, BuildNumber, IsReleased, CodeCoverage, HasPassedCodeCoverageCheck, ' +
17
- 'Package2.IsOrgDependent, ReleaseVersion, BuildDurationInSeconds, HasMetadataRemoved, CreatedById ' +
17
+ 'Package2.IsOrgDependent, ReleaseVersion, BuildDurationInSeconds, HasMetadataRemoved, CreatedById, ConvertedFromVersionId ' +
18
18
  'FROM Package2Version ' +
19
19
  "WHERE Id = '%s' AND IsDeprecated != true " +
20
20
  'ORDER BY Package2Id, Branch, MajorVersion, MinorVersion, PatchVersion, BuildNumber';
@@ -194,3 +194,15 @@ The version number is required and was not found in the options or in package js
194
194
  # missingConnection
195
195
 
196
196
  A connection is required.
197
+
198
+ # IdUnavailableWhenQueued
199
+
200
+ Request is queued. ID unavailable.
201
+
202
+ # IdUnavailableWhenInProgress
203
+
204
+ Request is in progress. ID unavailable.
205
+
206
+ # IdUnavailableWhenError
207
+
208
+ ID Unavailable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/packaging",
3
- "version": "2.2.9-profiles.0",
3
+ "version": "2.3.0",
4
4
  "description": "Packaging library for the Salesforce packaging platform",
5
5
  "main": "lib/exported",
6
6
  "types": "lib/exported.d.ts",
@@ -42,18 +42,21 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "@oclif/core": "^2.9.4",
45
- "@salesforce/core": "^4.3.10",
46
- "@salesforce/kit": "^3.0.5",
45
+ "@salesforce/core": "^4.3.2",
46
+ "@salesforce/kit": "^3.0.3",
47
47
  "@salesforce/schemas": "^1.6.0",
48
- "@salesforce/source-deploy-retrieve": "^9.3.4",
48
+ "@salesforce/source-deploy-retrieve": "^8.6.0",
49
49
  "@salesforce/ts-types": "^2.0.3",
50
- "fast-xml-parser": "^4.2.5",
50
+ "@xmldom/xmldom": "^0.8.9",
51
+ "debug": "^4.3.4",
51
52
  "globby": "^11",
52
53
  "graphology": "^0.25.1",
53
54
  "graphology-traversal": "^0.3.1",
54
55
  "graphology-types": "^0.24.7",
55
- "jsforce": "^2.0.0-beta.27",
56
+ "js2xmlparser": "^4.0.2",
57
+ "jsforce": "^2.0.0-beta.23",
56
58
  "jszip": "^3.10.1",
59
+ "ts-retry-promise": "^0.7.0",
57
60
  "xml2js": "^0.6.0"
58
61
  },
59
62
  "devDependencies": {
@@ -61,20 +64,22 @@
61
64
  "@salesforce/dev-config": "^4.0.1",
62
65
  "@salesforce/dev-scripts": "^5.4.3",
63
66
  "@salesforce/prettier-config": "^0.0.3",
64
- "@salesforce/ts-sinon": "^1.4.11",
67
+ "@salesforce/ts-sinon": "^1.4.12",
68
+ "@types/debug": "4.1.8",
65
69
  "@types/globby": "^9.1.0",
66
70
  "@types/jszip": "^3.4.1",
67
71
  "@types/xml2js": "^0.4.11",
68
72
  "@typescript-eslint/eslint-plugin": "^5.60.0",
69
73
  "@typescript-eslint/parser": "^5.61.0",
70
74
  "chai": "^4.3.7",
75
+ "commitizen": "^4.2.6",
71
76
  "eslint": "^8.44.0",
72
77
  "eslint-config-prettier": "^8.8.0",
73
78
  "eslint-config-salesforce": "^2.0.1",
74
79
  "eslint-config-salesforce-license": "^0.2.0",
75
80
  "eslint-config-salesforce-typescript": "^1.1.1",
76
81
  "eslint-plugin-header": "3.1.1",
77
- "eslint-plugin-import": "^2.27.5",
82
+ "eslint-plugin-import": "^2.28.0",
78
83
  "eslint-plugin-jsdoc": "^44.2.7",
79
84
  "eslint-plugin-sf-plugin": "^1.15.9",
80
85
  "husky": "^8.0.3",
@@ -1,31 +0,0 @@
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 {};
@@ -1,101 +0,0 @@
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