UNPKG

@salesforce/packaging

Version:

Packaging library for the Salesforce packaging platform

879 lines 54.5 kB
"use strict"; /* * Copyright (c) 2022, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateVersionNumber = exports.validateAncestorId = exports.packageXmlJsonToXmlString = exports.packageXmlStringToPackageXmlJson = exports.MetadataResolver = exports.PackageVersionCreate = void 0; const node_path_1 = __importDefault(require("node:path")); const node_os_1 = __importDefault(require("node:os")); const node_fs_1 = __importDefault(require("node:fs")); const core_1 = require("@salesforce/core"); const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve"); const project_1 = require("@salesforce/core/project"); const kit_1 = require("@salesforce/kit"); const fast_xml_parser_1 = require("fast-xml-parser"); const ts_types_1 = require("@salesforce/ts-types"); const pkgUtils = __importStar(require("../utils/packageUtils")); const packageUtils_1 = require("../utils/packageUtils"); const interfaces_1 = require("../interfaces"); const packageProfileApi_1 = require("./packageProfileApi"); const packageVersionCreateRequest_1 = require("./packageVersionCreateRequest"); const package_1 = require("./package"); const versionNumber_1 = require("./versionNumber"); core_1.Messages.importMessagesDirectory(__dirname); const messages = core_1.Messages.loadMessages('@salesforce/packaging', 'package_version_create'); const DESCRIPTOR_FILE = 'package2-descriptor.json'; class PackageVersionCreate { options; apiVersionFromPackageXml; project; connection; packageObject; packageId; pkg; logger; metadataResolver; constructor(options) { this.options = options; this.connection = this.options.connection; this.project = this.options.project; this.logger = core_1.Logger.childFromRoot('packageVersionCreate'); this.metadataResolver = new MetadataResolver(); } createPackageVersion() { try { return this.packageVersionCreate(); } catch (err) { if (err instanceof Error) { throw pkgUtils.applyErrorAction(pkgUtils.massageErrorMessage(err)); } throw err; } } /** * A dependency in the project config file may be specified using either a subscriber package version id (04t) * or a package Id (0Ho) + a version number. Additionally, a build number may be the actual build number, or a * keyword: LATEST or RELEASED (meaning the latest or released build number for a given major.minor.patch). * * This method resolves a package Id + version number to a subscriber package version id (04t) * and adds it as a SubscriberPackageVersionId parameter in the dependency object. */ async retrieveSubscriberPackageVersionId(dependency) { if (!dependency.versionNumber) { throw messages.createError('errorDependencyPair', [JSON.stringify(dependency)]); } if (!dependency.packageId) { throw messages.createError('errorDependencyPair', [JSON.stringify(dependency)]); } const versionNumber = versionNumber_1.VersionNumber.from(dependency.versionNumber); const buildNumber = versionNumber.build; // use the dependency.branch if present otherwise use the branch of the version being created const branch = dependency.branch ?? this.options.branch; const branchString = !branch ? 'null' : `'${branch}'`; // resolve a build number keyword to an actual number, if needed const resolvedBuildNumber = await this.resolveBuildNumber(versionNumber, dependency.packageId, branch); // now that we have a full build number, query for the associated 04t. // because the build number may not be unique across versions, add in conditionals for // the branch or the RELEASED token (if used) const branchOrReleasedCondition = buildNumber === versionNumber_1.BuildNumberToken.RELEASED_BUILD_NUMBER_TOKEN ? 'AND IsReleased = true' : `AND Branch = ${branchString}`; const query = `SELECT SubscriberPackageVersionId FROM Package2Version WHERE Package2Id = '${dependency.packageId}' AND MajorVersion = ${versionNumber.major} AND MinorVersion = ${versionNumber.minor} AND PatchVersion = ${versionNumber.patch} AND BuildNumber = ${resolvedBuildNumber} ${branchOrReleasedCondition}`; const pkgVerQueryResult = await this.connection.tooling.query(query); const subRecords = pkgVerQueryResult.records; if (!subRecords || subRecords.length !== 1) { throw messages.createError('versionNumberNotFoundInDevHub', [ dependency.packageId, branchString, versionNumber.toString(), resolvedBuildNumber, ]); } // warn user of the resolved build number when LATEST and RELEASED keywords are used if (versionNumber.isBuildKeyword()) { versionNumber.build = resolvedBuildNumber; if (buildNumber === versionNumber_1.BuildNumberToken.LATEST_BUILD_NUMBER_TOKEN) { this.logger.info(messages.getMessage('buildNumberResolvedForLatest', [ dependency.package, versionNumber.toString(), branchString, dependency.subscriberPackageVersionId, ])); } else if (buildNumber === versionNumber_1.BuildNumberToken.RELEASED_BUILD_NUMBER_TOKEN) { this.logger.info(messages.getMessage('buildNumberResolvedForReleased', [ dependency.package, versionNumber.toString(), dependency.subscriberPackageVersionId, ])); } } return pkgVerQueryResult.records[0].SubscriberPackageVersionId; } /** side effect: removes properties from the passed in dependency! */ async resolveSubscriberPackageVersionId(dependency) { await this.validateDependencyValues(dependency); if (dependency.subscriberPackageVersionId) { delete dependency.package; // if a 04t id is specified just use it. return dependency; } dependency.subscriberPackageVersionId = await this.retrieveSubscriberPackageVersionId(dependency); delete dependency.packageId; delete dependency.package; delete dependency.versionNumber; delete dependency.branch; return dependency; } async validateDependencyValues(dependency) { // If valid 04t package, just return it to be used straight away. if (dependency.subscriberPackageVersionId) { pkgUtils.validateId(pkgUtils.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID, dependency.subscriberPackageVersionId); return; } if (dependency.packageId && dependency.package) { throw messages.createError('errorPackageAndPackageIdCollision', []); } const idOrPackage = dependency.packageId ?? dependency.package; if (!idOrPackage) { throw messages.createError('errorPackageOrPackageIdMissing', []); } const packageIdFromAlias = this.project.getPackageIdFromAlias(idOrPackage) ?? idOrPackage; // If valid 04t package, just return it to be used straight away. if (pkgUtils.validateIdNoThrow(pkgUtils.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID, packageIdFromAlias)) { dependency.subscriberPackageVersionId = packageIdFromAlias; return; } if (!packageIdFromAlias || !dependency.versionNumber) { throw messages.createError('errorDependencyPair', [JSON.stringify(dependency)]); } // Just override dependency.packageId value to the resolved alias. dependency.packageId = packageIdFromAlias; pkgUtils.validateId(pkgUtils.BY_LABEL.PACKAGE_ID, dependency.packageId); (0, exports.validateVersionNumber)(dependency.versionNumber, versionNumber_1.BuildNumberToken.LATEST_BUILD_NUMBER_TOKEN, versionNumber_1.BuildNumberToken.RELEASED_BUILD_NUMBER_TOKEN); await this.validatePatchVersion(dependency.versionNumber, dependency.packageId); // Validate that the Package2 id exists on the server const query = `SELECT Id FROM Package2 WHERE Id = '${dependency.packageId}'`; const result = await this.connection.tooling.query(query); if (!result.records || result.records.length !== 1) { throw messages.createError('errorNoIdInHub', [dependency.packageId]); } } async resolveBuildNumber(versionNumber, packageId, branch) { if (!versionNumber.isBuildKeyword()) { // The build number is already specified so just return it using the tooling query result obj structure return `${versionNumber.build}`; } // query for the LATEST or RELEASED build number (excluding deleted versions) let branchCondition = ''; let releasedCondition = ''; if (versionNumber.build === versionNumber_1.BuildNumberToken.LATEST_BUILD_NUMBER_TOKEN) { // respect the branch when querying for LATEST const branchString = !branch || branch === '' ? 'null' : `'${branch}'`; branchCondition = `AND Branch = ${branchString}`; } else if (versionNumber.build === versionNumber_1.BuildNumberToken.RELEASED_BUILD_NUMBER_TOKEN) { releasedCondition = 'AND IsReleased = true'; } const query = `SELECT MAX(BuildNumber) FROM Package2Version WHERE Package2Id = '${packageId}' AND IsDeprecated != true AND MajorVersion = ${versionNumber.major} AND MinorVersion = ${versionNumber.minor} AND PatchVersion = ${versionNumber.patch} ${branchCondition} ${releasedCondition}`; const results = await this.connection.tooling.query(query); if (results.records?.length === 0 || results.records[0].expr0 == null) { if (versionNumber.build === versionNumber_1.BuildNumberToken.RELEASED_BUILD_NUMBER_TOKEN) { throw messages.createError('noReleaseVersionFound', [packageId, versionNumber.toString()]); } else { throw messages.createError('noReleaseVersionFoundForBranch', [packageId, branch, versionNumber.toString()]); } } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${results.records[0].expr0}`; } async createRequestObject(preserveFiles, packageVersTmpRoot, packageVersBlobZipFile) { const zipFileBase64 = node_fs_1.default.readFileSync(packageVersBlobZipFile).toString('base64'); const requestObject = { Package2Id: this.packageId, VersionInfo: zipFileBase64, Tag: this.options.tag, Branch: this.options.branch, InstallKey: this.options.installationkey, Instance: this.options.buildinstance, SourceOrg: this.options.sourceorg, CalculateCodeCoverage: this.options.codecoverage ?? false, SkipValidation: this.options.skipvalidation ?? false, AsyncValidation: this.options.asyncvalidation ?? false, Language: this.options.language, // note: the createRequest's Language corresponds to the AllPackageVersion's language CalcTransitiveDependencies: this.packageObject.calculateTransitiveDependencies ?? false, }; this.stripUnsupportedPropertiesBasedOnApiVersion(requestObject); if (preserveFiles) { const message = messages.getMessage('tempFileLocation', [packageVersTmpRoot]); await core_1.Lifecycle.getInstance().emit(interfaces_1.PackageVersionEvents.create['preserve-files'], { location: packageVersTmpRoot, message, }); this.logger.info(message); return requestObject; } else { return node_fs_1.default.promises.rm(packageVersTmpRoot, { recursive: true, force: true }).then(() => requestObject); } } // TODO: W-18696754 - None of the logger.warn() calls work, nothing is printed to the console when they're hit. stripUnsupportedPropertiesBasedOnApiVersion(requestObject) { if (this.connection.getApiVersion() < '57.0') { if (requestObject.Language) { this.logger.warn(`The language option is only valid for API version 57.0 and higher. Ignoring ${requestObject.Language}`); } delete requestObject.Language; } if (this.connection.getApiVersion() < '60.0') { if (requestObject.AsyncValidation) { this.logger.warn(`The async validation option is only valid for API version 60.0 and higher. Ignoring ${requestObject.AsyncValidation}`); } delete requestObject.AsyncValidation; } if (this.connection.getApiVersion() < '65.0') { if (requestObject.CalcTransitiveDependencies) { this.logger.warn(`The CalculateTransitiveDependencies option is only valid for API version 65.0 and higher. Ignoring ${requestObject.CalcTransitiveDependencies}`); } delete requestObject.CalcTransitiveDependencies; } } /** * Convert the list of command line options to a JSON object that can be used to create an Package2VersionCreateRequest entity. * * @returns {{Package2Id: (*|p|boolean), Package2VersionMetadata: *, Tag: *, Branch: number}} * @private */ async createPackageVersionCreateRequestFromOptions() { const preserveFiles = !!(this.options.preserve ?? kit_1.env.getBoolean('SF_PACKAGE2_VERSION_CREATE_PRESERVE')); const uniqueHash = (0, packageUtils_1.uniqid)({ template: `${this.packageId}-%s` }); const packageVersTmpRoot = node_path_1.default.join(node_os_1.default.tmpdir(), `${uniqueHash}`); const packageVersMetadataFolder = node_path_1.default.join(packageVersTmpRoot, 'md-files'); const unpackagedMetadataFolder = node_path_1.default.join(packageVersTmpRoot, 'unpackaged-md-files'); const seedMetadataFolder = node_path_1.default.join(packageVersTmpRoot, 'seed-md-files'); const packageVersProfileFolder = node_path_1.default.join(packageVersMetadataFolder, 'profiles'); const packageVersBlobDirectory = node_path_1.default.join(packageVersTmpRoot, 'package-version-info'); const metadataZipFile = node_path_1.default.join(packageVersBlobDirectory, 'package.zip'); const unpackagedMetadataZipFile = node_path_1.default.join(packageVersBlobDirectory, 'unpackaged-metadata-package.zip'); const seedMetadataZipFile = node_path_1.default.join(packageVersBlobDirectory, 'seed-metadata-package.zip'); const settingsZipFile = node_path_1.default.join(packageVersBlobDirectory, 'settings.zip'); const packageVersBlobZipFile = node_path_1.default.join(packageVersTmpRoot, 'package-version-info.zip'); const sourceBaseDir = node_path_1.default.join(this.project.getPath(), this.packageObject?.path ?? ''); const mdOptions = { deploydir: packageVersMetadataFolder, sourceDir: sourceBaseDir, sourceApiVersion: this.project?.getSfProjectJson()?.get('sourceApiVersion'), }; await node_fs_1.default.promises.mkdir(packageVersBlobDirectory, { recursive: true }); const settingsGenerator = new core_1.ScratchOrgSettingsGenerator({ asDirectory: true }); let packageDescriptorJson = structuredClone(this.packageObject); const apvLanguage = packageDescriptorJson.language; // Copy all the metadata from the workspace to a tmp folder const componentSet = await this.metadataResolver.generateMDFolderForArtifact(mdOptions); this.verifyHasSource(componentSet); if (packageDescriptorJson.package) { delete packageDescriptorJson.package; packageDescriptorJson.id = this.packageId; } if (packageDescriptorJson.language) { // the structuredClone() call above added the packageDir's language to the descriptor; // remove that from the descriptor here; it will be set correctly from the definitionFile values below delete packageDescriptorJson.language; } const definitionFile = this.options.definitionfile ? this.options.definitionfile : packageDescriptorJson.definitionFile; if (definitionFile) { // package2-descriptor.json sent to the server should contain only the features, snapshot & orgPreferences // defined in the definition file. delete packageDescriptorJson.features; delete packageDescriptorJson.orgPreferences; delete packageDescriptorJson.definitionFile; delete packageDescriptorJson.snapshot; const definitionFilePayload = await node_fs_1.default.promises.readFile(definitionFile, 'utf8'); const definitionFileJson = JSON.parse(definitionFilePayload); // Load any settings from the definition await settingsGenerator.extract(definitionFileJson); if (settingsGenerator.hasSettings() && definitionFileJson.orgPreferences) { // this is not allowed, exit with an error throw messages.createError('signupDuplicateSettingsSpecified'); } packageDescriptorJson = (0, packageUtils_1.copyDescriptorProperties)(packageDescriptorJson, definitionFileJson); } this.resolveBuildUserPermissions(packageDescriptorJson); // All dependencies for the packaging dir should be resolved to an 04t id to be passed to the server. // (see resolveSubscriberPackageVersionId() for details) const dependencies = packageDescriptorJson.dependencies; // branch and APV language can be set via options or packageDirectory; option takes precedence this.options.branch = this.options.branch ?? packageDescriptorJson.branch; this.options.language = this.options.language ?? apvLanguage; const resultValues = await Promise.all(!dependencies ? [] : dependencies.map((dependency) => this.resolveSubscriberPackageVersionId(dependency))); const versionNumber = this.options.versionnumber ?? packageDescriptorJson.versionNumber; if (!versionNumber) { throw messages.createError('versionNumberRequired'); } const ancestorId = await this.getAncestorId(packageDescriptorJson, this.options.project, versionNumber, !!this.options.skipancestorcheck); // If dependencies exist, the resultValues array will contain the dependencies populated with a resolved // subscriber pkg version id. if (resultValues.length > 0) { packageDescriptorJson.dependencies = resultValues; } packageDescriptorJson = cleanPackageDescriptorJson(packageDescriptorJson); packageDescriptorJson = setPackageDescriptorJsonValues(packageDescriptorJson, this.options, this.logger); await node_fs_1.default.promises.mkdir(packageVersTmpRoot, { recursive: true }); await node_fs_1.default.promises.mkdir(packageVersBlobDirectory, { recursive: true }); if (Reflect.has(packageDescriptorJson, 'ancestorVersion')) { delete packageDescriptorJson.ancestorVersion; } packageDescriptorJson.ancestorId = ancestorId; await node_fs_1.default.promises.writeFile(node_path_1.default.join(packageVersBlobDirectory, DESCRIPTOR_FILE), JSON.stringify(packageDescriptorJson), 'utf-8'); await this.cleanGeneratedPackage({ packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, settingsGenerator, }); return this.createRequestObject(preserveFiles, packageVersTmpRoot, packageVersBlobZipFile); } verifyHasSource(result) { if (!result.converted || result.converted?.length === 0) { throw messages.createError('noSourceInRootDirectory', [this.packageObject.path ?? '<unknown>']); } } async cleanGeneratedPackage({ packageVersMetadataFolder, packageVersProfileFolder, unpackagedMetadataFolder, seedMetadataFolder, metadataZipFile, settingsZipFile, packageVersBlobDirectory, packageVersBlobZipFile, unpackagedMetadataZipFile, seedMetadataZipFile, settingsGenerator, }) { // As part of the source convert process, the package.xml has been written into the tmp metadata directory. // The package.xml may need to be manipulated due to processing profiles in the workspace or additional // metadata exclusions. If necessary, read the existing package.xml and then re-write it. const currentPackageXml = await node_fs_1.default.promises.readFile(node_path_1.default.join(packageVersMetadataFolder, 'package.xml'), 'utf8'); // convert to json const packageXmlAsJson = (0, exports.packageXmlStringToPackageXmlJson)(currentPackageXml); if (!packageXmlAsJson) { throw messages.createError('packageXmlDoesNotContainPackage'); } if (!packageXmlAsJson?.types) { throw messages.createError('packageXmlDoesNotContainPackageTypes'); } node_fs_1.default.mkdirSync(packageVersMetadataFolder, { recursive: true }); node_fs_1.default.mkdirSync(packageVersProfileFolder, { recursive: true }); this.apiVersionFromPackageXml = packageXmlAsJson.version; const sourceApiVersion = this.project?.getSfProjectJson()?.get('sourceApiVersion'); const hasSeedMetadata = await this.metadataResolver.resolveMetadata(this.packageObject.seedMetadata?.path, seedMetadataFolder, 'seedMDDirectoryDoesNotExist', sourceApiVersion); let hasUnpackagedMetadata = false; const unpackagedMetadataPath = this.packageObject.unpackagedMetadata?.path; if (this.options.codecoverage) { hasUnpackagedMetadata = await this.metadataResolver.resolveMetadata(unpackagedMetadataPath, unpackagedMetadataFolder, 'unpackagedMDDirectoryDoesNotExist', sourceApiVersion); } let profileExcludeDirs = []; if (this.packageObject.scopeProfiles) { this.logger.debug(`packageDirectory: ${this.packageObject.name} has 'scopeProfiles' set, so only including profiles from within this directory`); // exclude all package dirs except the one being packaged profileExcludeDirs = this.project .getPackageDirectories() .map((packageDir) => { if (packageDir.path !== this.packageObject.path) { return packageDir.path; } }) .filter((packageDirPath) => packageDirPath); } else { // don't package the profiles from any un-packagedMetadata dir in the project profileExcludeDirs = this.project .getPackageDirectories() .filter(project_1.isNamedPackagingDirectory) .map((packageDir) => packageDir.unpackagedMetadata?.path) .filter(ts_types_1.isString); let debugMsg = 'Searching for profiles to include from all packageDirectories'; if (profileExcludeDirs?.length) { debugMsg += ` excluding these unpackagedMetadata dirs: ${profileExcludeDirs.toString()}`; } this.logger.debug(debugMsg); } const typesArr = this.options?.profileApi?.filterAndGenerateProfilesForManifest(packageXmlAsJson.types, profileExcludeDirs) ?? packageXmlAsJson.types; // Next generate profiles and retrieve any profiles that were excluded because they had no matching nodes. const excludedProfiles = this.options?.profileApi?.generateProfiles(packageVersProfileFolder, typesArr, profileExcludeDirs); packageXmlAsJson.types = typesArr.map((type) => { if (type.name !== 'Profile') return type; return { ...type, members: type.members.filter((m) => !excludedProfiles?.includes(m)) }; }); const xml = (0, exports.packageXmlJsonToXmlString)(packageXmlAsJson); await node_fs_1.default.promises.writeFile(node_path_1.default.join(packageVersMetadataFolder, 'package.xml'), xml, 'utf-8'); // Zip the packageVersMetadataFolder folder and put the zip in {packageVersBlobDirectory}/package.zip await (0, packageUtils_1.zipDir)(packageVersMetadataFolder, metadataZipFile); if (hasSeedMetadata) { // Zip the seedMetadataFolder folder and put the zip in {packageVersBlobDirectory}/{seedMetadataZipFile} await (0, packageUtils_1.zipDir)(seedMetadataFolder, seedMetadataZipFile); } if (hasUnpackagedMetadata) { // Zip the unpackagedMetadataFolder folder and put the zip in {packageVersBlobDirectory}/{unpackagedMetadataZipFile} await (0, packageUtils_1.zipDir)(unpackagedMetadataFolder, unpackagedMetadataZipFile); } // Zip up the expanded settings (if present) if (settingsGenerator.hasSettings()) { await settingsGenerator.createDeploy(); await settingsGenerator.createDeployPackageContents(this.apiVersionFromPackageXml); await (0, packageUtils_1.zipDir)(`${settingsGenerator.getDestinationPath() ?? ''}${node_path_1.default.sep}${settingsGenerator.getShapeDirName()}`, settingsZipFile); } // Zip the Version Info and package.zip files into another zip await (0, packageUtils_1.zipDir)(packageVersBlobDirectory, packageVersBlobZipFile); } /** side effect: modifies the passed in parameter! */ resolveBuildUserPermissions(packageDescriptorJson) { // Process permissionSet and permissionSetLicenses that should be enabled when running Apex tests // This only applies if code coverage is enabled if (this.options.codecoverage) { // Assuming no permission sets are named 0, 0n, null, undefined, false, NaN, and the empty string if (packageDescriptorJson.apexTestAccess?.permissionSets) { let permSets = packageDescriptorJson.apexTestAccess.permissionSets; if (!Array.isArray(permSets)) { permSets = permSets.split(','); } packageDescriptorJson.permissionSetNames = permSets.map((s) => s.trim()); } if (packageDescriptorJson.apexTestAccess?.permissionSetLicenses) { let permissionSetLicenses = packageDescriptorJson.apexTestAccess.permissionSetLicenses; if (!Array.isArray(permissionSetLicenses)) { permissionSetLicenses = permissionSetLicenses.split(','); } packageDescriptorJson.permissionSetLicenseDeveloperNames = permissionSetLicenses.map((s) => s.trim()); } } // Process permissionSet and permissionsetLicenses that should be enabled for the package metadata deploy if (packageDescriptorJson.packageMetadataAccess?.permissionSets) { let permSets = packageDescriptorJson.packageMetadataAccess.permissionSets; if (!Array.isArray(permSets)) { permSets = permSets.split(','); } packageDescriptorJson.packageMetadataPermissionSetNames = permSets.map((s) => s.trim()); } if (packageDescriptorJson.packageMetadataAccess?.permissionSetLicenses) { let permissionSetLicenses = packageDescriptorJson.packageMetadataAccess.permissionSetLicenses; if (!Array.isArray(permissionSetLicenses)) { permissionSetLicenses = permissionSetLicenses.split(','); } packageDescriptorJson.packageMetadataPermissionSetLicenseNames = permissionSetLicenses.map((s) => s.trim()); } delete packageDescriptorJson.apexTestAccess; delete packageDescriptorJson.packageMetadataAccess; } // eslint-disable-next-line complexity async packageVersionCreate() { // For the first rollout of validating sfdx-project.json data against schema, make it optional and defaulted // to false. Validation only occurs if the optional validateschema option has been specified. if (this.options.validateschema) { await this.project.getSfProjectJson().schemaValidate(); } // Check for empty packageDirectories if (this.project.getPackageDirectories()?.length === 0) { throw messages.createError('errorEmptyPackageDirs'); } // either options.packageId or options.path is required if (!this.options.packageId && !this.options.path) { throw messages.createError('errorMissingPackageIdOrPath'); } // establish the package Id (0ho) and load the package directory let packageName; let packageObject; if (this.options.packageId) { const pkg = this.options.packageId; // for backward compatibility allow for a packageDirectory package property to be an id (0Ho) instead of an alias. packageName = (await this.getPackageDirFromId(pkg))?.package; if (!packageName) { packageName = pkg.startsWith('0Ho') ? this.project.getAliasesFromPackageId(pkg).find((alias) => alias) : pkg; if (!packageName) throw messages.createError('errorMissingPackage', [this.options.packageId]); } packageObject = this.project.findPackage((namedPackageDir) => (0, project_1.isPackagingDirectory)(namedPackageDir) && (namedPackageDir.package === packageName || namedPackageDir.name === packageName)); } else { // We'll either have a package ID or alias, or a directory path if (!this.options.path) { throw messages.createError('errorMissingPackagePath', [JSON.stringify(this.options)]); } packageObject = this.project.getPackageFromPath(this.options.path); if (!packageObject || !(0, project_1.isPackagingDirectory)(packageObject)) throw messages.createError('errorCouldNotFindPackageUsingPath', [this.options.path]); packageName = packageObject?.package; } if (!packageObject) { throw messages.createError('errorCouldNotFindPackageDir', [ this.options.packageId ? 'packageId or alias' : 'path', this.options.packageId ?? this.options.path, ]); } else { this.packageObject = packageObject; } this.packageId = this.project.getPackageIdFromAlias(packageName) ?? packageName; this.options.profileApi = await packageProfileApi_1.PackageProfileApi.create({ project: this.project, includeUserLicenses: !!this.packageObject.includeProfileUserLicenses, }); // At this point, the packageIdFromAlias should have been resolved to an Id. Now, we // need to validate that the Id is correct. pkgUtils.validateId(pkgUtils.BY_LABEL.PACKAGE_ID, this.packageId); try { await this.validateOptionsForPackageType(); } catch (error) { if (error instanceof Error) { if (error.name === 'NOT_FOUND') { // this means the 0Ho package was not found in the org. throw a better error. throw messages.createError('errorNoIdInHub', [this.packageId]); } } throw error; } const request = await this.createPackageVersionCreateRequestFromOptions(); const options = { multipartFileFields: { VersionInfo: { contentType: 'application/zip', filename: 'package-version-info.zip', }, }, }; const createResult = await this.connection.tooling.create('Package2VersionCreateRequest', request, options); if (!createResult.success) { const errStr = createResult.errors?.join(', ') ?? createResult.errors; const id = createResult.id ?? ''; throw messages.createError('failedToCreatePVCRequest', [id === '' ? '' : ` [${id}]`, errStr.toString()]); } return (await (0, packageVersionCreateRequest_1.byId)(createResult.id, this.connection))[0]; } async getPackageDirFromId(pkg) { if (pkg.startsWith('0Ho')) { const dir = (await this.project.getSfProjectJson().getPackageDirectories()) .filter(project_1.isPackagingDirectory) .filter((p) => p.package === pkg); if (dir.length === 1) { return dir[0]; } } } async getPackageType() { // this.packageId should be an 0Ho package Id at this point if (!this.pkg) { this.pkg = new package_1.Package({ packageAliasOrId: this.packageId, project: this.project, connection: this.connection, }); } return this.pkg.getType(); } async validateOptionsForPackageType() { if ((await this.getPackageType()) === 'Unlocked') { // Don't allow scripts in unlocked packages if (Boolean(this.options.postinstallscript) || this.options.uninstallscript) { throw messages.createError('errorScriptsNotApplicableToUnlockedPackage'); } // Don't allow ancestor in unlocked packages if (Boolean(this.packageObject.ancestorId) || this.packageObject.ancestorVersion) { throw messages.createError('errorAncestorNotApplicableToUnlockedPackage'); } } } async validatePatchVersion(versionNumberString, packageId) { const query = `SELECT ContainerOptions FROM Package2 WHERE id ='${packageId}'`; const queryResult = await this.connection.tooling.query(query); if (queryResult.records === null || queryResult.records.length === 0) { throw messages.createError('errorInvalidPackageId', [packageId]); } // Enforce a patch version of zero (0) for Locked packages only if (queryResult.records[0].ContainerOptions === 'Locked') { const versionNumber = versionNumber_1.VersionNumber.from(versionNumberString); if (versionNumber.patch !== '0') { throw messages.createError('errorInvalidPatchNumber', [versionNumberString]); } } } // eslint-disable-next-line complexity async getAncestorId(packageDescriptorJson, project, versionNumberString, skipAncestorCheck) { // If an id property is present, use it. Otherwise, look up the package id from the package property. const packageId = packageDescriptorJson.id ?? project.getPackageIdFromAlias(packageDescriptorJson.package ?? '') ?? packageDescriptorJson.package; // No need to proceed if Unlocked if ((await this.getPackageType()) === 'Unlocked') { return ''; } let ancestorId = ''; // ancestorID can be alias, 05i, or 04t; // validate and convert to 05i, as needed const versionNumber = versionNumber_1.VersionNumber.from(versionNumberString); let origSpecifiedAncestor = packageDescriptorJson.ancestorId ?? ''; let highestReleasedVersion = null; const explicitUseHighestRelease = packageDescriptorJson.ancestorId === versionNumber_1.BuildNumberToken.HIGHEST_VERSION_NUMBER_TOKEN || packageDescriptorJson.ancestorVersion === versionNumber_1.BuildNumberToken.HIGHEST_VERSION_NUMBER_TOKEN; const explicitUseNoAncestor = packageDescriptorJson.ancestorId === versionNumber_1.BuildNumberToken.NONE_VERSION_NUMBER_TOKEN || packageDescriptorJson.ancestorVersion === versionNumber_1.BuildNumberToken.NONE_VERSION_NUMBER_TOKEN; if ((explicitUseHighestRelease || explicitUseNoAncestor) && packageDescriptorJson.ancestorId && packageDescriptorJson.ancestorVersion) { if (packageDescriptorJson.ancestorId !== packageDescriptorJson.ancestorVersion) { // both ancestorId and ancestorVersion specified, HIGHEST and/or NONE are used, the values disagree throw messages.createError('errorAncestorIdVersionHighestOrNoneMismatch', [ packageDescriptorJson.ancestorId, packageDescriptorJson.ancestorVersion, ]); } } if (explicitUseNoAncestor && skipAncestorCheck) { return ''; } else { const result = await this.getAncestorIdHighestRelease(packageId, versionNumberString, explicitUseHighestRelease, skipAncestorCheck); if (result.finalAncestorId) { return result.finalAncestorId; } highestReleasedVersion = result.highestReleasedVersion; } // at this point if explicitUseHighestRelease=true, we have returned the ancestorId or thrown an error // highestReleasedVersion should be null only if skipAncestorCheck or if there is no existing released package version if (!explicitUseNoAncestor && packageDescriptorJson.ancestorId) { ancestorId = project.getPackageIdFromAlias(packageDescriptorJson.ancestorId) ?? packageDescriptorJson.ancestorId; (0, packageUtils_1.validateId)([packageUtils_1.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID, packageUtils_1.BY_LABEL.PACKAGE_VERSION_ID], ancestorId); ancestorId = (await (0, packageUtils_1.getPackageVersionId)(ancestorId, this.connection)) ?? ''; } if (!explicitUseNoAncestor && packageDescriptorJson.ancestorVersion) { const regNumbers = new RegExp('^[0-9]+$'); const versionNumberSplit = packageDescriptorJson.ancestorVersion.split(packageUtils_1.VERSION_NUMBER_SEP); if (versionNumberSplit.length < 3 || versionNumberSplit.length > 4 || !versionNumberSplit[0].match(regNumbers) || !versionNumberSplit[1].match(regNumbers) || !versionNumberSplit[2].match(regNumbers)) { throw new Error(messages.getMessage('errorInvalidAncestorVersionFormat', [packageDescriptorJson.ancestorVersion])); } const query = 'SELECT Id, IsReleased FROM Package2Version ' + `WHERE Package2Id = '${packageId ?? ''}' AND MajorVersion = ${versionNumberSplit[0]} AND MinorVersion = ${versionNumberSplit[1]} AND PatchVersion = ${versionNumberSplit[2]}`; let queriedAncestorId; const ancestorVersionResult = await this.connection.tooling.query(query); if (!ancestorVersionResult.totalSize) { throw messages.createError('errorNoMatchingAncestor', [packageDescriptorJson.ancestorVersion, packageId]); } else { const releasedAncestor = ancestorVersionResult.records.find((rec) => rec.IsReleased === true); if (!releasedAncestor) { throw messages.createError('errorAncestorNotReleased', [packageDescriptorJson.ancestorVersion]); } else { queriedAncestorId = releasedAncestor.Id; } } // check for discrepancy between queried ancestorId and descriptor's ancestorId if (packageDescriptorJson?.ancestorId && ancestorId !== queriedAncestorId) { throw messages.createError('errorAncestorIdVersionMismatch', [ packageDescriptorJson.ancestorVersion, packageDescriptorJson.ancestorId, ]); } ancestorId = queriedAncestorId; origSpecifiedAncestor = packageDescriptorJson.ancestorVersion; } return (0, exports.validateAncestorId)(ancestorId, highestReleasedVersion, explicitUseNoAncestor, versionNumber.patch !== '0', skipAncestorCheck, origSpecifiedAncestor); } async getAncestorIdHighestRelease(packageId, versionNumberString, explicitUseHighestRelease, skipAncestorCheck) { if (!packageId) { throw messages.createError('packageIdCannotBeUndefined'); } const versionNumber = versionNumberString.split(packageUtils_1.VERSION_NUMBER_SEP); const isPatch = versionNumber[2] !== '0'; const result = { finalAncestorId: null, highestReleasedVersion: null, }; if (isPatch && explicitUseHighestRelease) { // based on server-side validation, whatever ancestor is specified for a patch is // tightly controlled; therefore we only need concern ourselves if explicitUseHighestRelease == true; // equally applies when skipAncestorCheck == true // gather appropriate matching major.minor.0 const query = `SELECT Id FROM Package2Version WHERE Package2Id = '${packageId}' ` + 'AND IsReleased = True AND IsDeprecated = False AND PatchVersion = 0 ' + `AND MajorVersion = ${versionNumber[0]} AND MinorVersion = ${versionNumber[1]} ` + 'ORDER BY MajorVersion Desc, MinorVersion Desc, PatchVersion Desc, BuildNumber Desc LIMIT 1'; const majorMinorVersionResult = await this.connection.tooling.query(query); const majorMinorVersionRecords = majorMinorVersionResult.records; if (majorMinorVersionRecords && majorMinorVersionRecords?.length === 1 && majorMinorVersionRecords[0]) { result.finalAncestorId = majorMinorVersionRecords[0].Id ?? null; } else { const majorMinorNotFound = `${versionNumber[0]}.${versionNumber[1]}.0`; throw messages.createError('errorNoMatchingMajorMinorForPatch', [majorMinorNotFound]); } } else if (!isPatch && (explicitUseHighestRelease || !skipAncestorCheck)) { // ancestor must be set to latest released major.minor version const query = 'SELECT Id, SubscriberPackageVersionId, MajorVersion, MinorVersion, PatchVersion FROM Package2Version ' + `WHERE Package2Id = '${packageId}' AND IsReleased = True AND IsDeprecated = False AND PatchVersion = 0 ` + 'ORDER BY MajorVersion Desc, MinorVersion Desc, PatchVersion Desc, BuildNumber Desc LIMIT 1'; const highestVersionResult = await this.connection.tooling.query(query); const highestVersionRecords = highestVersionResult.records; if (highestVersionRecords?.[0]) { result.highestReleasedVersion = highestVersionRecords[0]; if (explicitUseHighestRelease) { result.finalAncestorId = result.highestReleasedVersion.Id; } } else if (explicitUseHighestRelease) { // there is no eligible ancestor version throw messages.createError('errorNoMatchingAncestor', [versionNumberString, packageId]); } } return result; } } exports.PackageVersionCreate = PackageVersionCreate; class MetadataResolver { async resolveMetadata(metadataRelativePath, metadataOutputPath, errorMessageLabel, sourceApiVersion) { if (metadataRelativePath) { const metadataFullPath = node_path_1.default.join(process.cwd(), metadataRelativePath); if (!node_fs_1.default.existsSync(metadataFullPath)) { throw messages.createError(errorMessageLabel, [metadataRelativePath]); } node_fs_1.default.mkdirSync(metadataOutputPath, { recursive: true }); await this.generateMDFolderForArtifact({ deploydir: metadataOutputPath, sourceDir: metadataFullPath, sourceApiVersion, }); return true; } return false; } // convert source to mdapi format and copy to tmp dir packaging up async generateMDFolderForArtifact(options) { const sourcePaths = (0, kit_1.ensureArray)(options.sourcePaths ?? options.sourceDir ? options.sourceDir : undefined).filter((srcPath) => srcPath); const componentSetOptions = { sourceapiversion: options.sourceApiVersion, ...(sourcePaths.length > 0 ? { sourcepath: sourcePaths } : {}), }; if (!options.deploydir) { throw messages.createError('deploydirCannotBeUndefined', [JSON.stringify(options)]); } const componentSet = await source_deploy_retrieve_1.ComponentSetBuilder.build(componentSetOptions); const packageName = options.packageName; const outputDirectory = node_path_1.default.resolve(options.deploydir); const convertResult = await this.convertMetadata(componentSet, outputDirectory, packageName); if (packageName) { // SDR will build an output path like /output/directory/packageName/package.xml // this was breaking from toolbelt, so to revert it we copy the directory up a level and delete the original if (!convertResult.packagePath) { throw messages.createError('packagePathCannotBeUndefined'); } (0, packageUtils_1.copyDir)(convertResult.packagePath, outputDirectory); try { node_fs_1.default.rmSync(convertResult.packagePath, { recursive: true }); } catch (e) { // rmdirSync is being deprecated and emits a warning // but rmSync is introduced in node 14 so fall back to rmdirSync node_fs_1.default.rmdirSync(convertResult.packagePath, { recursive: true }); } convertResult.packagePath = outputDirectory; } return convertResult; } /** * Extracted into a method for UT purposes * * @param componentSet CS to convert * @param outputDirectory where to place the converted MD * @param packageName the packagename related to the CS * @private */ // eslint-disable-next-line class-methods-use-this async convertMetadata(componentSet, outputDirectory, packageName) { const converter = new source_deploy_retrieve_1.MetadataConverter(); return converter.convert(componentSet, 'metadata', { type: 'directory', outputDirectory, packageName, genUniqueDir: false, }); } } exports.MetadataResolver = MetadataResolver; const packageXmlStringToPackageXmlJson = (rawXml) => { const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: true, parseTagValue: false, parseAttributeValue: false, cdataPropName: '__cdata', ignoreDeclaration: true, numberParseOptions: { leadingZeros: false, hex: false }, // make sure types and members is always an array isArray: (name) => ['types', 'members'].includes(name), }); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return parser.parse(rawXml).Package; }; exports.packageXmlStringToPackageXmlJson = packageXmlStringToPackageXmlJson; /** * Converts PackageXmlJson to a string representing the Xml * */ const packageXmlJsonToXmlString = (packageXmlJson) => { const builder = new fast_xml_parser_1.XMLBuilder({ format: true, indentBy: ' ', ignoreAttributes: false, cdataPropName: '__cdata', processEntities: false, attributeNamePrefix: '@@@', }); return String(builder.build({ '?xml': { '@@@version': '1.0', '@@@encoding