@salesforce/packaging
Version:
Packaging library for the Salesforce packaging platform
462 lines • 21.1 kB
JavaScript
;
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