@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
165 lines • 8.25 kB
JavaScript
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
;