UNPKG

@salesforce/packaging

Version:

Packaging library for the Salesforce packaging platform

462 lines 21.1 kB
"use strict"; 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.BY_LABEL = exports.INSTALL_URL_BASE = exports.VERSION_NUMBER_SEP = void 0; exports.uniqid = uniqid; exports.validateId = validateId; exports.validateIdNoThrow = validateIdNoThrow; exports.applyErrorAction = applyErrorAction; exports.massageErrorMessage = massageErrorMessage; exports.getPackageVersionId = getPackageVersionId; exports.escapeInstallationKey = escapeInstallationKey; exports.getContainerOptions = getContainerOptions; exports.getPackageVersionStrings = getPackageVersionStrings; exports.queryWithInConditionChunking = queryWithInConditionChunking; exports.getPackageVersionNumber = getPackageVersionNumber; exports.generatePackageAliasEntry = generatePackageAliasEntry; exports.combineSaveErrors = combineSaveErrors; exports.numberToDuration = numberToDuration; exports.zipDir = zipDir; exports.copyDir = copyDir; exports.copyDescriptorProperties = copyDescriptorProperties; exports.isPackageDirectoryEffectivelyEmpty = isPackageDirectoryEffectivelyEmpty; /* * 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 */ const node_os_1 = __importDefault(require("node:os")); const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = require("node:path"); const node_stream_1 = require("node:stream"); const node_util_1 = __importStar(require("node:util")); const node_crypto_1 = require("node:crypto"); const core_1 = require("@salesforce/core"); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const globby_1 = __importDefault(require("globby")); const jszip_1 = __importDefault(require("jszip")); core_1.Messages.importMessagesDirectory(__dirname); const messages = core_1.Messages.loadMessages('@salesforce/packaging', 'pkg_utils'); exports.VERSION_NUMBER_SEP = '.'; const INVALID_TYPE_REGEX = /[\w]*(sObject type '[A-Za-z]*Package[2]?[A-Za-z]*' is not supported)[\w]*/im; const ID_REGISTRY = [ { prefix: '0Ho', label: 'Package Id', }, { prefix: '05i', label: 'Package Version Id', }, { prefix: '08c', label: 'Package Version Create Request Id', }, { prefix: '04t', label: 'Subscriber Package Version Id', }, ]; exports.INSTALL_URL_BASE = new core_1.SfdcUrl('https://login.salesforce.com/packaging/installPackage.apexp?p0='); // https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_soslsoql.htm const SOQL_WHERE_CLAUSE_MAX_LENGTH = 4000; exports.BY_LABEL = (() => Object.fromEntries(ID_REGISTRY.map((id) => [id.label.replace(/ /g, '_').toUpperCase(), { prefix: id.prefix, label: id.label }])))(); /** * A function to generate a unique id and return it in the context of a template, if supplied. * * A template is a string that can contain `${%s}` to be replaced with a unique id. * If the template contains the "%s" placeholder, it will be replaced with the unique id otherwise the id will be appended to the template. * * @param options an object with the following properties: * - template: a template string. * - length: the length of the unique id as presented in hexadecimal. */ function uniqid(options) { const uniqueString = (0, node_crypto_1.randomBytes)(Math.ceil((options?.length ?? 32) / 2.0)) .toString('hex') .slice(0, options?.length ?? 32); if (!options?.template) { return uniqueString; } return options.template.includes('%s') ? node_util_1.default.format(options.template, uniqueString) : `${options.template}${uniqueString}`; } function validateId(idObj, value) { if (!value || !validateIdNoThrow(idObj, value)) { throw messages.createError('invalidIdOrAlias', [ Array.isArray(idObj) ? idObj.map((e) => e.label).join(' or ') : idObj.label, value, Array.isArray(idObj) ? idObj.map((e) => e.prefix).join(' or ') : idObj.prefix, ]); } } function validateIdNoThrow(idObj, value) { if (!value || (value.length !== 15 && value.length !== 18)) { return false; } return Array.isArray(idObj) ? idObj.some((e) => value.startsWith(e.prefix)) : value.startsWith(idObj.prefix); } // applies actions to common package errors // eslint-disable-next-line complexity function applyErrorAction(err) { // append when actions already exist const actions = []; // include existing actions if (err.action) { actions.push(err.action); } // TODO: (need to get with packaging team on this) // until next generation packaging is GA, wrap perm-based errors w/ // 'contact sfdc' action (REMOVE once GA'd) if ((err.name === 'INVALID_TYPE' && INVALID_TYPE_REGEX.test(err.message)) || (err.name === 'NOT_FOUND' && err.message === messages.getMessage('notFoundMessage'))) { // contact sfdc customer service actions.push(messages.getMessage('packageNotEnabledAction')); } if (err.name === 'INVALID_FIELD' && err.message.includes('Instance')) { actions.push(messages.getMessage('packageInstanceNotEnabled')); } if (err.name === 'INVALID_FIELD' && err.message.includes('SourceOrg')) { actions.push(messages.getMessage('packageSourceOrgNotEnabled')); } if (err.name === 'INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST') { actions.push(messages.getMessage('invalidPackageTypeAction')); } if (err.name === 'MALFORMED_ID' && err.message === messages.getMessage('malformedPackageIdMessage')) { actions.push(messages.getMessage('malformedPackageIdAction')); } if (err.name === 'MALFORMED_ID' && err.message === messages.getMessage('malformedPackageVersionIdMessage')) { actions.push(messages.getMessage('malformedPackageVersionIdAction')); } if ((err.message.includes(exports.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID.label) && err.message.includes('is invalid')) || err.name === 'INVALID_ID_FIELD' || (err.name === 'INVALID_INPUT' && err.message.includes('Verify you entered the correct ID')) || err.name === 'MALFORMED_ID') { actions.push(messages.getMessage('idNotFoundAction')); } if (actions.length > 0) { err['action'] = actions.join('\n'); } return err; } function massageErrorMessage(err) { if (err.name === 'INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST') { err['message'] = messages.getMessage('invalidPackageTypeMessage'); } if (err.name === 'MALFORMED_ID' && (err.message.includes('Version ID') || err.message.includes('Version Definition ID'))) { err['message'] = messages.getMessage('malformedPackageVersionIdMessage'); } if (err.name === 'MALFORMED_ID' && err.message.includes('Package2 ID')) { err['message'] = messages.getMessage('malformedPackageIdMessage'); } // remove references to Second Generation if (err.message.includes('Second Generation ')) { err['message'] = err.message.replace('Second Generation ', ''); } return err; } /** * Given a subscriber package version ID (04t) or package version ID (05i), return the package version ID (05i) * * @param versionId The subscriber package version ID * @param connection For tooling query */ async function getPackageVersionId(versionId, connection) { // if it's already a 05i return it, otherwise query for it if (versionId?.startsWith(exports.BY_LABEL.PACKAGE_VERSION_ID.prefix)) { return versionId; } const query = `SELECT Id FROM Package2Version WHERE SubscriberPackageVersionId = '${versionId}'`; return connection.tooling.query(query).then((queryResult) => { if (!queryResult?.totalSize) { throw messages.createError('errorInvalidIdNoMatchingVersionId', [ exports.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID.label, versionId, exports.BY_LABEL.PACKAGE_VERSION_ID.label, ]); } return queryResult.records[0].Id; }); } function escapeInstallationKey(key) { return key.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } /** * Get the ContainerOptions for the specified Package2 (0Ho) IDs. * * @return Map of 0Ho id to container option api value * @param packageIds The list of package IDs * @param connection For tooling query */ // eslint-disable-next-line @typescript-eslint/require-await async function getContainerOptions(packageIds, connection) { const ids = (0, kit_1.ensureArray)(packageIds).filter(ts_types_1.isString); if (ids.length === 0) { return new Map(); } const query = "SELECT Id, ContainerOptions FROM Package2 WHERE Id IN ('%IDS%')"; const records = await queryWithInConditionChunking(query, ids, '%IDS%', connection); if (records && records.length > 0) { return new Map(records.map((record) => [record.Id, record.ContainerOptions])); } return new Map(); } /** * Given a list of subscriber package version IDs (04t), return the associated version strings (e.g., Major.Minor.Patch.Build) * * @return Map of subscriberPackageVersionId to versionString * @param subscriberPackageVersionIds * @param connection For tooling query */ // eslint-disable-next-line @typescript-eslint/require-await async function getPackageVersionStrings(subscriberPackageVersionIds, connection) { let results = new Map(); if (!subscriberPackageVersionIds || subscriberPackageVersionIds.length === 0) { return results; } // remove any duplicate Ids const ids = [...new Set(subscriberPackageVersionIds)]; const query = "SELECT SubscriberPackageVersionId, MajorVersion, MinorVersion, PatchVersion, BuildNumber FROM Package2Version WHERE SubscriberPackageVersionId IN ('%IDS%')"; const records = await queryWithInConditionChunking(query, ids, '%IDS%', connection); if (records && records.length > 0) { results = new Map(records.map((record) => { const version = concatVersion(record.MajorVersion, record.MinorVersion, record.PatchVersion, record.BuildNumber); return [record.SubscriberPackageVersionId, version]; })); } return results; } /** * For queries with an IN condition, determine if the WHERE clause will exceed * SOQL's 4000 character limit. Perform multiple queries as needed to stay below the limit. * * @return concatenated array of records returned from the resulting query(ies) * @param query The full query to execute containing the replaceToken param in its IN clause * @param items The IN clause items. A length-appropriate single-quoted comma-separated string chunk will be made from the items. * @param replaceToken A placeholder in the query's IN condition that will be replaced with the chunked items * @param connection For tooling query */ async function queryWithInConditionChunking(query, items, replaceToken, connection) { let records = []; if (!query || !items || !replaceToken) { return records; } const whereClause = query.substring(query.toLowerCase().indexOf('where'), query.length); const inClauseItemsMaxLength = SOQL_WHERE_CLAUSE_MAX_LENGTH - whereClause.length - replaceToken.length; let itemsQueried = 0; while (itemsQueried < items.length) { const chunkCount = getInClauseItemsCount(items, itemsQueried, inClauseItemsMaxLength); if (chunkCount === 0) { throw messages.createError('itemDoesNotFitWithinMaxLength', [ query, items[itemsQueried].slice(0, 20), items[itemsQueried].length, inClauseItemsMaxLength, ]); } const itemsStr = `${items.slice(itemsQueried, itemsQueried + chunkCount).join("','")}`; const queryChunk = query.replace(replaceToken, itemsStr); // eslint-disable-next-line no-await-in-loop const result = await connection.tooling.query(queryChunk); if (result && result.records.length > 0) { records = records.concat(result.records); } itemsQueried += chunkCount; } return records; } /** * Returns the number of items that can be included in a quoted comma-separated string (e.g., "'item1','item2'") not exceeding maxLength */ // TODO: this function cannot handle a single item that is longer than maxLength - what to do, since this could be the root cause of an infinite loop? function getInClauseItemsCount(items, startIndex, maxLength) { let resultLength = 0; let includedCount = 0; while (startIndex + includedCount < items.length) { let itemLength = 0; if (items[startIndex + includedCount]) { itemLength = items[startIndex + includedCount].length + 3; // 3 = length of "''," if (resultLength + itemLength > maxLength) { // the limit has been exceeded, return the current count return includedCount; } includedCount++; resultLength += itemLength; } } return includedCount; } /** * Return a version string in Major.Minor.Patch.Build format, using 0 for any empty part */ function concatVersion(major, minor, patch, build) { return [major, minor, patch, build].map((part) => (part ? `${part}` : '0')).join('.'); } function getPackageVersionNumber(package2VersionObj, includeBuild = false) { const version = concatVersion(package2VersionObj.MajorVersion, package2VersionObj.MinorVersion, package2VersionObj.PatchVersion, includeBuild ? package2VersionObj.BuildNumber : undefined); return includeBuild ? version : version.slice(0, version.lastIndexOf('.')); } /** * Generate package alias json entry for this package version that can be written to sfdx-project.json * * @param connection * @param project SfProject instance for the project * @param packageVersionId 04t id of the package to create the alias entry for * @param packageVersionNumber that will be appended to the package name to form the alias * @param branch * @param packageId the 0Ho id * @private */ // TODO: move sfProject async function generatePackageAliasEntry(connection, project, packageVersionId, packageVersionNumber, branch, packageId) { const aliasForPackageId = project.getAliasesFromPackageId(packageId); let packageName; if (aliasForPackageId?.length === 0) { const query = `SELECT Name FROM Package2 WHERE Id = '${packageId}'`; const package2 = await connection.singleRecordQuery(query, { tooling: true }); packageName = package2.Name; } else { packageName = aliasForPackageId[0]; } const packageAlias = branch ? `${packageName}@${packageVersionNumber}-${branch}` : `${packageName}@${packageVersionNumber}`; return [packageAlias, packageVersionId]; } function combineSaveErrors(sObject, crudOperation, errors) { const errorMessages = errors.map((error) => { const fieldsString = error.fields?.length ? `Fields: [${error.fields?.join(', ')}]` : ''; return `Error: ${error.errorCode} Message: ${error.message} ${fieldsString}`; }); return messages.createError('errorDuringSObjectCRUDOperation', [crudOperation, sObject, errorMessages.join(node_os_1.default.EOL)]); } /** * Returns a Duration object from param duration when it is a number, otherwise return itself * * @param duration = number to be converted to a Duration or Duration object * @param unit = (Default Duration.Unit.MILLISECONDS) Duration unit of number - See @link {Duration.Unit} for valid values */ function numberToDuration(duration, unit = kit_1.Duration.Unit.MILLISECONDS) { if (duration === undefined) { return new kit_1.Duration(0, unit); } return (0, ts_types_1.isNumber)(duration) ? new kit_1.Duration(duration, unit) : duration; } const pipeline = (0, node_util_1.promisify)(node_stream_1.pipeline); /** * Zips directory to given zipfile. * * @param dir directory to zip * @param zipfile path to the zip file to create */ async function zipDir(dir, zipfile) { const logger = core_1.Logger.childFromRoot('srcDevUtils#zipDir'); const timer = process.hrtime(); const globbyResult = await (0, globby_1.default)('**/*', { expandDirectories: true, cwd: dir }); const zip = new jszip_1.default(); // add files tp zip for (const file of globbyResult) { zip.file(file, node_fs_1.default.readFileSync((0, node_path_1.join)(dir, file))); } // write zip to file const zipStream = zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true, compression: 'DEFLATE', compressionOptions: { level: 3, }, }); await pipeline(zipStream, node_fs_1.default.createWriteStream(zipfile)); const stat = node_fs_1.default.statSync(zipfile); logger.debug(`${stat.size} bytes written to ${zipfile} in ${getElapsedTime(timer)}ms`); return; } function getElapsedTime(timer) { const elapsed = process.hrtime(timer); return (elapsed[0] * 1000 + elapsed[1] / 1_000_000).toFixed(3); } function copyDir(src, dest) { node_fs_1.default.mkdirSync(dest, { recursive: true }); const entries = node_fs_1.default.readdirSync(src, { withFileTypes: true }); entries.map((entry) => { const srcPath = (0, node_path_1.join)(src, entry.name); const destPath = (0, node_path_1.join)(dest, entry.name); return entry.isDirectory() ? copyDir(srcPath, destPath) : node_fs_1.default.copyFileSync(srcPath, destPath); }); } /** * Parse and copy properties from both of these arguments into a new object * * @param packageDescriptorJson * @param definitionFileJson * @returns the resulting object with specific properties * overridden from definition file based on case-insensitive matches. */ function copyDescriptorProperties(packageDescriptorJson, definitionFileJson) { const packageDescriptorJsonCopy = structuredClone(packageDescriptorJson); const definitionFileJsonCopy = structuredClone(definitionFileJson); return Object.assign({}, packageDescriptorJsonCopy, Object.fromEntries(['country', 'edition', 'language', 'features', 'orgPreferences', 'snapshot', 'release', 'sourceOrg'].map((prop) => { const matchCase = Object.keys(definitionFileJsonCopy).find((key) => key.toLowerCase() === prop.toLowerCase()); return [[prop], matchCase ? definitionFileJsonCopy[matchCase] : undefined]; }))); } /** * Brand new SFDX projects contain a force-app directory tree containing empty folders * and a few .eslintrc.json files. We still want to consider such a directory tree * as 'empty' for the sake of operations like downloading package version metadata. * * @param directory The absolute path to a directory * @returns true if the directory contains nothing except empty directories or * directories containing only an .eslintrc.json file. */ function isPackageDirectoryEffectivelyEmpty(directory) { if (!node_fs_1.default.lstatSync(directory).isDirectory()) { return false; } const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true }); return entries.every((entry) => entry.isDirectory() ? isPackageDirectoryEffectivelyEmpty((0, node_path_1.join)(directory, entry.name)) : entry.name === '.eslintrc.json'); } //# sourceMappingURL=packageUtils.js.map