UNPKG

@salesforce/source-tracking

Version:

API for tracking local and remote Salesforce metadata changes

155 lines 8.95 kB
"use strict"; /* * 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