@salesforce/source-tracking
Version:
API for tracking local and remote Salesforce metadata changes
155 lines • 8.95 kB
JavaScript
;
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.uniqueArrayConcat = exports.changeResultToMetadataComponent = exports.FileResponseSuccessToRemoteSyncInput = exports.remoteChangeToMetadataMember = exports.getAllFiles = exports.sourceComponentHasFullNameAndType = exports.sourceComponentIsCustomLabel = exports.forceIgnoreDenies = exports.deleteCustomLabels = exports.ensureRelative = exports.chunkArray = exports.folderContainsPath = exports.pathIsInFolder = exports.excludeLwcLocalOnlyTest = exports.supportsPartialDelete = exports.getKeyFromObject = exports.getMetadataNameFromLegacyKey = exports.getMetadataTypeFromLegacyKey = exports.getMetadataNameFromKey = exports.getMetadataTypeFromKey = exports.getLegacyMetadataKey = exports.getMetadataKey = void 0;
const node_path_1 = require("node:path");
const node_fs_1 = __importDefault(require("node:fs"));
const ts_types_1 = require("@salesforce/ts-types");
const fast_xml_parser_1 = require("fast-xml-parser");
const kit_1 = require("@salesforce/kit");
const remoteChangeIgnoring_1 = require("./remoteChangeIgnoring");
const keySplit = '###';
const legacyKeySplit = '__';
const getMetadataKey = (metadataType, metadataName) => `${metadataType}${keySplit}${metadataName}`;
exports.getMetadataKey = getMetadataKey;
const getLegacyMetadataKey = (metadataType, metadataName) => `${metadataType}${legacyKeySplit}${metadataName}`;
exports.getLegacyMetadataKey = getLegacyMetadataKey;
const getMetadataTypeFromKey = (key) => decodeURIComponent(key.split(keySplit)[0]);
exports.getMetadataTypeFromKey = getMetadataTypeFromKey;
const getMetadataNameFromKey = (key) => decodeURIComponent(key.split(keySplit)[1]);
exports.getMetadataNameFromKey = getMetadataNameFromKey;
const getMetadataTypeFromLegacyKey = (key) => key.split(legacyKeySplit)[0];
exports.getMetadataTypeFromLegacyKey = getMetadataTypeFromLegacyKey;
const getMetadataNameFromLegacyKey = (key) => decodeURIComponent(key.split(legacyKeySplit).slice(1).join(legacyKeySplit));
exports.getMetadataNameFromLegacyKey = getMetadataNameFromLegacyKey;
const getKeyFromObject = (element) => {
if (element.type && element.name) {
return (0, exports.getMetadataKey)(element.type, element.name);
}
throw new Error(`unable to complete key from ${JSON.stringify(element)}`);
};
exports.getKeyFromObject = getKeyFromObject;
const supportsPartialDelete = (cmp) => !!cmp.type.supportsPartialDelete;
exports.supportsPartialDelete = supportsPartialDelete;
const excludeLwcLocalOnlyTest = (filePath) => !(filePath.includes('__utam__') || filePath.includes('__tests__'));
exports.excludeLwcLocalOnlyTest = excludeLwcLocalOnlyTest;
/**
* Verify that a filepath starts exactly with a complete parent path
* ex: '/foo/bar-extra/baz'.startsWith('foo/bar') would be true, but this function understands that they are not in the same folder
*/
const pathIsInFolder = (folder) => (filePath) => {
if (folder === filePath) {
return true;
}
// use sep to ensure a folder like foo will not match a filePath like foo-bar
// comparing foo/ to foo-bar/ ensure this.
const normalizedFolderPath = (0, node_path_1.normalize)(`${folder}${node_path_1.sep}`);
const normalizedFilePath = (0, node_path_1.normalize)(`${filePath}${node_path_1.sep}`);
if (normalizedFilePath.startsWith(normalizedFolderPath)) {
return true;
}
const filePathParts = normalizedFilePath.split(node_path_1.sep).filter(nonEmptyStringFilter);
return normalizedFolderPath
.split(node_path_1.sep)
.filter(nonEmptyStringFilter)
.every((part, index) => part === filePathParts[index]);
};
exports.pathIsInFolder = pathIsInFolder;
/** just like pathIsInFolder but with the parameter order reversed for iterating a single file against an array of folders */
const folderContainsPath = (filePath) => (folder) => (0, exports.pathIsInFolder)(folder)(filePath);
exports.folderContainsPath = folderContainsPath;
const nonEmptyStringFilter = (value) => (0, ts_types_1.isString)(value) && value.length > 0;
// adapted for TS from https://github.com/30-seconds/30-seconds-of-code/blob/master/snippets/chunk.md
const chunkArray = (arr, size) => Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => arr.slice(i * size, i * size + size));
exports.chunkArray = chunkArray;
const ensureRelative = (projectPath) => (filePath) => (0, node_path_1.isAbsolute)(filePath) ? (0, node_path_1.relative)(projectPath, filePath) : filePath;
exports.ensureRelative = ensureRelative;
/**
* A method to help delete custom labels from a file, or the entire file if there are no more labels
*
* @param filename - a path to a custom labels file
* @param customLabels - an array of SourceComponents representing the custom labels to delete
* @returns -json equivalent of the custom labels file's contents OR undefined if the file was deleted/not written
*/
const deleteCustomLabels = async (filename, customLabels) => {
const customLabelsToDelete = new Set(customLabels.filter(exports.sourceComponentIsCustomLabel).map((change) => change.fullName));
// if we don't have custom labels, we don't need to do anything
if (!customLabelsToDelete.size) {
return undefined;
}
// for custom labels, we need to remove the individual label from the xml file
// so we'll parse the xml
const parser = new fast_xml_parser_1.XMLParser({
ignoreDeclaration: false,
ignoreAttributes: false,
attributeNamePrefix: '@_',
});
const cls = parser.parse(node_fs_1.default.readFileSync(filename, 'utf8'));
// delete the labels from the json based on their fullName's
cls.CustomLabels.labels = (0, kit_1.ensureArray)(cls.CustomLabels.labels).filter((label) => !customLabelsToDelete.has(label.fullName));
if (cls.CustomLabels.labels.length === 0) {
// we've deleted everything, so let's delete the file
await node_fs_1.default.promises.unlink(filename);
return undefined;
}
else {
// we need to write the file json back to xml back to the fs
const builder = new fast_xml_parser_1.XMLBuilder({
attributeNamePrefix: '@_',
ignoreAttributes: false,
format: true,
indentBy: ' ',
});
// and then write that json back to xml and back to the fs
const xml = builder.build(cls);
await node_fs_1.default.promises.writeFile(filename, xml);
return cls;
}
};
exports.deleteCustomLabels = deleteCustomLabels;
/** returns true if forceIgnore denies a path OR if there is no forceIgnore provided */
const forceIgnoreDenies = (forceIgnore) => (filePath) => forceIgnore?.denies(filePath) ?? false;
exports.forceIgnoreDenies = forceIgnoreDenies;
const sourceComponentIsCustomLabel = (input) => input.type.name === 'CustomLabel';
exports.sourceComponentIsCustomLabel = sourceComponentIsCustomLabel;
const sourceComponentHasFullNameAndType = (input) => typeof input.fullName === 'string' && typeof input.type.name === 'string';
exports.sourceComponentHasFullNameAndType = sourceComponentHasFullNameAndType;
const getAllFiles = (sc) => [sc.xml, ...sc.walkContent()].filter(ts_types_1.isString);
exports.getAllFiles = getAllFiles;
const remoteChangeToMetadataMember = (cr) => {
const checked = (0, remoteChangeIgnoring_1.ensureNameAndType)(cr);
return {
fullName: checked.name,
type: checked.type,
};
};
exports.remoteChangeToMetadataMember = remoteChangeToMetadataMember;
// weird, right? This is for oclif.table which allows types but not interfaces. In this case, they are equivalent
const FileResponseSuccessToRemoteSyncInput = (fr) => fr;
exports.FileResponseSuccessToRemoteSyncInput = FileResponseSuccessToRemoteSyncInput;
const changeResultToMetadataComponent = (registry) => (cr) => ({
fullName: cr.name,
type: registry.getTypeByName(cr.type),
});
exports.changeResultToMetadataComponent = changeResultToMetadataComponent;
// TODO: use set.union when node 22 is everywhere
const uniqueArrayConcat = (arr1, arr2) => Array.from(new Set([...arr1, ...arr2]));
exports.uniqueArrayConcat = uniqueArrayConcat;
//# sourceMappingURL=functions.js.map