UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

165 lines 8.25 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NonDecompositionFinalizer = void 0; /* * Copyright (c) 2023, 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_path_1 = require("node:path"); const ts_types_1 = require("@salesforce/ts-types"); const core_1 = require("@salesforce/core"); const decomposed_1 = require("../../utils/decomposed"); const constants_1 = require("../../common/constants"); const componentSet_1 = require("../../collections/componentSet"); const treeContainers_1 = require("../../resolve/treeContainers"); const streams_1 = require("../streams"); const transactionFinalizer_1 = require("./transactionFinalizer"); /** * Merges child components that share the same parent in the conversion pipeline * into a single file. * * Inserts unclaimed child components into the parent that belongs to the default package */ class NonDecompositionFinalizer extends transactionFinalizer_1.ConvertTransactionFinalizer { transactionState = { childrenByUniqueElement: new Map(), exampleComponent: undefined, }; // filename => (childName => childXml) mergeMap = new Map(); // filename => sourceComponent parentComponentMap = new Map(); tree; async finalize(defaultDirectory, tree = new treeContainers_1.NodeFSTreeContainer()) { const writerData = []; if (this.transactionState.childrenByUniqueElement.size === 0) { return writerData; } this.tree = tree; const packageDirectories = core_1.SfProject.getInstance(defaultDirectory).getPackageDirectories(); const pkgPaths = packageDirectories.map((pkg) => pkg.fullPath); // nondecomposed metadata types can exist in multiple locations under the same name // so we have to find all components that could potentially match inbound components if (!this.transactionState.exampleComponent) { throw new Error('No example component exists in the transaction state for nondecomposed metadata'); } const allNonDecomposed = pkgPaths.includes(defaultDirectory) ? this.getAllComponentsOfType(pkgPaths, this.transactionState.exampleComponent.type.name) : // defaultDirectory isn't a package, assume it's the target output dir for conversion so don't scan folder []; // prepare 3 maps to simplify component merging await this.initMergeMap(allNonDecomposed); this.parentComponentMap = new Map(allNonDecomposed.map((c) => [(0, ts_types_1.ensureString)(c.xml, `no xml file path for ${c.fullName}`), c])); const childNameToParentFilePath = this.initChildMapping(); // we'll merge any new labels into the default location const defaultKey = (0, node_path_1.join)(defaultDirectory, getDefaultOutput(this.transactionState.exampleComponent)); this.ensureDefaults(defaultKey); // put the incoming components into the mergeMap. Keep track of any files we need to write const filesToWrite = new Set(); this.transactionState.childrenByUniqueElement.forEach((child, childUniqueElement) => { const parentKey = childNameToParentFilePath.get(childUniqueElement) ?? defaultKey; const parentItemMap = this.mergeMap.get(parentKey); parentItemMap?.set(childUniqueElement, child); filesToWrite.add(parentKey); }); // use the mergeMap to return the writables this.mergeMap.forEach((children, parentKey) => { if (filesToWrite.has(parentKey)) { const parentSourceComponent = this.parentComponentMap.get(parentKey); if (!parentSourceComponent) { throw new Error(`No source component found for ${parentKey}`); } const recomposedXmlObj = recompose(children, parentSourceComponent); writerData.push({ component: parentSourceComponent, writeInfos: [{ source: new streams_1.JsToXml(recomposedXmlObj), output: parentKey }], }); } }); return writerData; } initChildMapping() { const output = new Map(); this.mergeMap.forEach((children, parentKey) => { children.forEach((child, childName) => { output.set(childName, parentKey); }); }); return output; } /** * check both top-level maps and make sure there are defaults */ ensureDefaults(defaultKey) { if (!this.mergeMap.has(defaultKey)) { // If project has no files of this type, there won't be anything from allNonDecomposed. this.mergeMap.set(defaultKey, new Map()); } if (!this.parentComponentMap.has(defaultKey)) { // it's possible to get here if there are no files of this type in the project. // we don't have any SourceComponent to reference except the new incoming ones // so this creates a "default" component with the correct path for the xml file this.parentComponentMap.set(defaultKey, { ...this.transactionState.exampleComponent, xml: defaultKey, }); } } /** * Returns all the components of the incoming type in the project. * * Some components are not resolved during component resolution. * This typically only happens when a specific source path was resolved. This is problematic for * nondecomposed metadata types (like CustomLabels) because we need to know the location of each * child type before recomposing the final xml. * The labels could belong in any of the files OR need to go in the default location which already contains labels */ getAllComponentsOfType(pkgDirs, componentType) { const unprocessedComponents = componentSet_1.ComponentSet.fromSource({ fsPaths: pkgDirs, include: new componentSet_1.ComponentSet([{ fullName: '*', type: componentType }]), tree: this.tree, }).getSourceComponents(); return unprocessedComponents.toArray(); } /** * Populate the mergeMap with all the children of all the local components */ async initMergeMap(allComponentsOfType) { // A function we can parallelize since we have to parseXml for each local file const getMappedChildren = async (component) => { const results = await Promise.all(component.getChildren().map(async (child) => { const childXml = await child.parseXml(); return [ (0, ts_types_1.getString)(childXml, (0, ts_types_1.ensureString)(child.type.uniqueIdElement), `No uniqueIdElement exists in the registry for ${child.type.name}`), childXml, ]; })); return new Map(results); }; const result = await Promise.all(allComponentsOfType.map(async (c) => [ (0, ts_types_1.ensureString)(c.xml, `Missing xml file for ${c.type.name}`), await getMappedChildren(c), ])); this.mergeMap = new Map(result); } } exports.NonDecompositionFinalizer = NonDecompositionFinalizer; /** Return a json object that's built up from the mergeMap children */ const recompose = (children, parentSourceComponent) => ({ [parentSourceComponent.type.name]: { [constants_1.XML_NS_KEY]: constants_1.XML_NS_URL, // for CustomLabels, that's `labels` [(0, decomposed_1.getXmlElement)(parentSourceComponent.type)]: Array.from(children.values()), }, }); /** Return the default filepath for new metadata of this type */ const getDefaultOutput = (component) => { const { fullName } = component; const [baseName] = fullName.split('.'); const output = `${baseName}.${component.type.suffix ?? ''}${constants_1.META_XML_SUFFIX}`; return (0, node_path_1.join)(component.getPackageRelativePath('', 'source'), output); }; //# sourceMappingURL=nonDecompositionFinalizer.js.map