@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
342 lines • 20.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceComponent = void 0;
/*
* Copyright (c) 2020, 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 sfError_1 = require("@salesforce/core/sfError");
const messages_1 = require("@salesforce/core/messages");
const lifecycle_1 = require("@salesforce/core/lifecycle");
const fast_xml_parser_1 = require("fast-xml-parser");
const ts_types_1 = require("@salesforce/ts-types");
const kit_1 = require("@salesforce/kit");
const metadata_1 = require("../utils/metadata");
const decomposed_1 = require("../utils/decomposed");
const path_1 = require("../utils/path");
const replacements_1 = require("../convert/replacements");
const types_1 = require("../collections/types");
const filePathGenerator_1 = require("../utils/filePathGenerator");
const treeContainers_1 = require("./treeContainers");
const forceIgnore_1 = require("./forceIgnore");
;
const messages = new messages_1.Messages('@salesforce/source-deploy-retrieve', 'sdr', new Map([["md_request_fail", "Metadata API request failed: %s"], ["error_convert_invalid_format", "Invalid conversion format '%s'"], ["error_could_not_infer_type", "%s: Could not infer a metadata type"], ["error_unexpected_child_type", "Unexpected child metadata [%s] found for parent type [%s]"], ["noParent", "Could not find parent type for %s (%s)"], ["error_expected_source_files", "%s: Expected source files for type '%s'"], ["error_failed_convert", "Component conversion failed: %s"], ["error_merge_metadata_target_unsupported", "Merge convert for metadata target format currently unsupported"], ["error_missing_adapter", "Missing adapter '%s' for metadata type '%s'"], ["error_missing_transformer", "Missing transformer '%s' for metadata type '%s'"], ["error_missing_type_definition", "Missing metadata type definition in registry for id '%s'."], ["error_missing_child_type_definition", "Type %s does not have a child type definition %s."], ["noChildTypes", "No child types found in registry for %s (reading %s at %s)"], ["error_no_metadata_xml_ignore", "Metadata xml file %s is forceignored but is required for %s."], ["noSourceIgnore", "%s metadata types require source files, but %s is forceignored."], ["noSourceIgnore.actions", "- Metadata types with content are composed of two files: a content file (ie MyApexClass.cls) and a -meta.xml file (i.e MyApexClass.cls-meta.xml). You must include both files in your .forceignore file. Or try appending \u201C\\*\u201D to your existing .forceignore entry.\n\nSee <https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm> for examples"], ["error_path_not_found", "%s: File or folder not found"], ["noContentFound", "SourceComponent %s (metadata type = %s) is missing its content file."], ["noContentFound.actions", ["Ensure the content file exists in the expected location.", "If the content file is in your .forceignore file, ensure the meta-xml file is also ignored to completely exclude it."]], ["error_parsing_xml", "SourceComponent %s (metadata type = %s) does not have an associated metadata xml to parse"], ["error_expected_file_path", "%s: path is to a directory, expected a file"], ["error_expected_directory_path", "%s: path is to a file, expected a directory"], ["error_directory_not_found_or_not_directory", "%s: path is not a directory"], ["error_no_directory_stream", "%s doesn't support readable streams on directories."], ["error_no_source_to_deploy", "No source-backed components present in the package."], ["error_no_components_to_retrieve", "No components in the package to retrieve."], ["error_static_resource_expected_archive_type", "A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s."], ["error_static_resource_missing_resource_file", "A StaticResource must have an associated .resource file, missing %s.resource-meta.xml"], ["error_no_job_id", "The %s operation is missing a job ID. Initialize an operation with an ID, or start a new job."], ["missingApiVersion", "Could not determine an API version to use for the generated manifest. Tried looking for sourceApiVersion in sfdx-project.json, apiVersion from config vars, and the highest apiVersion from the APEX REST endpoint. Using API version 58.0 as a last resort."], ["invalid_xml_parsing", "error parsing %s due to:\\n message: %s\\n line: %s\\n code: %s"], ["zipBufferError", "Zip buffer was not created during conversion"], ["undefinedComponentSet", "Unable to construct a componentSet. Check the logs for more information."], ["replacementsFileNotRead", "The file \"%s\" specified in the \"replacements\" property of sfdx-project.json could not be read."], ["unsupportedBundleType", "Unsupported Bundle Type: %s"], ["filePathGeneratorNoTypeSupport", "Type not supported for filepath generation: %s"], ["missingFolderType", "The registry has %s as is inFolder but it does not have a folderType"], ["tooManyFiles", "Multiple files found for path: %s."], ["cantGetName", "Unable to calculate fullName from path: %s (%s)"], ["missingMetaFileSuffix", "The metadata registry is configured incorrectly for %s. Expected a metaFileSuffix."], ["uniqueIdElementNotInRegistry", "No uniqueIdElement found in registry for %s (reading %s at %s)."], ["uniqueIdElementNotInChild", "The uniqueIdElement %s was not found the child (reading %s at %s)."], ["suggest_type_header", "A metadata type lookup for \"%s\" found the following close matches:"], ["suggest_type_did_you_mean", "-- Did you mean \".%s%s\" instead for the \"%s\" metadata type?"], ["suggest_type_more_suggestions", "Additional suggestions:\nConfirm the file name, extension, and directory names are correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"], ["type_name_suggestions", "Confirm the metadata type name is correct. Validate against the registry at:\n<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>\n\nIf the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:\n<https://developer.salesforce.com/docs/metadata-coverage>\n\nIf the type is available via Metadata API but not in the registry\n\n- Open an issue <https://github.com/forcedotcom/cli/issues>\n- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>"]]));
/**
* Representation of a MetadataComponent in a file tree.
*/
class SourceComponent {
name;
type;
xml;
parent;
parentType;
content;
replacements;
treeContainer;
forceIgnore;
markedForDelete = false;
destructiveChangesType;
pathContentMap = new Map();
constructor(props, tree = new treeContainers_1.NodeFSTreeContainer(), forceIgnore = new forceIgnore_1.ForceIgnore()) {
this.name = props.name;
this.type = props.type;
this.xml = props.xml;
this.parent = props.parent;
this.content = props.content;
this.parentType = props.parentType;
this.treeContainer = tree;
this.forceIgnore = forceIgnore;
}
get fullName() {
if (this.type.ignoreParsedFullName) {
return this.type.name;
}
if (this.parent && this.type.ignoreParentName) {
if (!this.name) {
throw new sfError_1.SfError(`Component was initialized without a name: ${this.xml ?? '<no xml>'} (${this.type.name})`);
}
return this.name;
}
else {
return `${this.parent ? `${this.parent.fullName}.` : ''}${this.name}`;
}
}
/**
* Gets the metafile path of this component. Not all the types have an XML metafile,
* e.g., DigitalExperience has a JSON metafile (_meta.json).
*
* @deprecated This function should not be used, use "xml" property instead.
* @returns The metafile path
*/
get metaFilePath() {
if (this.type.id === 'digitalexperience' && this.content && this.type.metaFileSuffix) {
// metaFileName = metaFileSuffix for DigitalExperience.
return (0, node_path_1.join)((0, node_path_1.dirname)(this.content), this.type.metaFileSuffix);
}
return this.xml;
}
get tree() {
return this.treeContainer;
}
/**
* Returns whether this component type is supported by the Metadata API
* and therefore should have an entry added to the manifest.
*
* This is defined on the type in the registry. The type is required to
* be in the registry for proper classification and for possible use in
* decomposition/recomposition.
*
* Default value is true, so the only way to return false is to explicitly
* set it in the registry as false.
*
* E.g., CustomFieldTranslation.
*/
get isAddressable() {
return this.type.isAddressable !== false;
}
/**
*
* @param props component properties (at a minimum, name and type)
* @param fs VirtualTree. If not provided, one will be constructed based on the name/type of the props
* @param forceIgnore
* @returns SourceComponent
*/
static createVirtualComponent(props, fs, forceIgnore) {
if (props.name) {
const tree = fs
? new treeContainers_1.VirtualTreeContainer(fs)
: treeContainers_1.VirtualTreeContainer.fromFilePaths((0, filePathGenerator_1.filePathsFromMetadataComponent)({ fullName: props.name, type: props.type }));
return new SourceComponent(props, tree, forceIgnore);
}
throw new sfError_1.SfError(`Virtual Components must be constructed with a name: ${props.type.name}`);
}
walkContent() {
const sources = [];
if (this.content) {
for (const fsPath of this.walk(this.content)) {
if (fsPath !== this.xml) {
sources.push(fsPath);
}
}
}
return sources;
}
/**
* returns the children of a parent SourceComponent
*
* Ensures that the children of SourceComponent are valid child types.
* Invalid child types can occur when projects are structured in an atypical way such as having
* ApexClasses or Layouts within a CustomObject folder.
*
* @return SourceComponent[] containing valid children
*/
getChildren() {
if (!this.parent && this.type.children) {
const validChildTypes = new Set(Object.keys(this.type.children.types));
const children = this.content ? this.getDecomposedChildren(this.content) : this.getNonDecomposedChildren();
// Ensure only valid child types are included with the parent.
children
.filter((child) => !validChildTypes.has(child.type?.id))
.map((child) => {
throw new sfError_1.SfError(messages.getMessage('error_unexpected_child_type', [child.xml ?? child.content, this.type.name]), 'TypeInferenceError');
});
return children;
}
return [];
}
async parseXml(xmlFilePath) {
const xml = xmlFilePath ?? this.xml;
if (xml) {
let contents;
if (this.pathContentMap.has(xml)) {
contents = this.pathContentMap.get(xml);
}
else {
contents = (await this.tree.readFile(xml)).toString();
this.pathContentMap.set(xml, contents);
}
const replacements = this.replacements?.[xml] ?? this.parent?.replacements?.[xml];
return this.parseAndValidateXML(replacements ? await (0, replacements_1.replacementIterations)(contents, replacements) : contents, xml);
}
return {};
}
parseXmlSync(xmlFilePath) {
const xml = xmlFilePath ?? this.xml;
if (xml) {
let contents;
if (this.pathContentMap.has(xml)) {
contents = this.pathContentMap.get(xml);
}
else {
contents = this.tree.readFileSync(xml).toString();
this.pathContentMap.set(xml, contents);
}
return this.parseAndValidateXML(contents, xml);
}
return {};
}
/**
* will return this instance of the forceignore, or will create one if undefined
*
* @return ForceIgnore
*/
getForceIgnore() {
return this.forceIgnore;
}
/**
* As a performance enhancement, use the already parsed parent xml source
* to return the child section of xml source. This is useful for non-decomposed
* transformers where all child source components reference the parent's
* xml file to prevent re-reading the same file multiple times.
*
* @param parentXml parsed parent XMl source as an object
* @returns child section of the parent's xml
*/
parseFromParentXml(parentXml) {
if (!this.parent) {
return parentXml;
}
const children = (0, kit_1.ensureArray)((0, ts_types_1.get)(parentXml, `${this.parent.type.name}.${(0, decomposed_1.getXmlElement)(this.type)}`));
const uniqueElement = this.type.uniqueIdElement;
const matched = uniqueElement
? children.find((c) => (0, ts_types_1.getString)(c, uniqueElement) === this.name) ??
parentXml[this.parent.type.name]
: parentXml[this.parent.type.name] ?? undefined;
if (!matched) {
throw new sfError_1.SfError(`Invalid XML tags or unable to find matching parent xml file for ${this.type.name} "${this.name}"`);
}
return matched;
}
getPackageRelativePath(fsPath, format) {
return (0, path_1.calculateRelativePath)(format)({ self: this.type, parentType: this.parentType })(this.fullName)(fsPath);
}
/**
* @returns whether this component should be part of destructive changes.
*/
isMarkedForDelete() {
return this.markedForDelete;
}
getDestructiveChangesType() {
return this.destructiveChangesType;
}
setMarkedForDelete(destructiveChangeType) {
if (destructiveChangeType === false) {
this.markedForDelete = false;
// unset destructiveChangesType if it was already set
delete this.destructiveChangesType;
}
else {
this.markedForDelete = true;
// destructiveChangeType is DestructiveChangeType OR boolean, if it's DestructiveChangesType.PRE => DestructiveChangesType.PRE
// if it's DestructiveChangesType.POST or 'true' => DestructiveChangesType.POST
this.destructiveChangesType =
destructiveChangeType === types_1.DestructiveChangesType.PRE ? types_1.DestructiveChangesType.PRE : types_1.DestructiveChangesType.POST;
}
}
parse(contents) {
const parsed = metadata_1.parser.parse(String(contents));
const [firstElement] = Object.keys(parsed);
if (firstElement === this.type.name) {
return parsed;
}
else if (this.parent) {
return this.parseFromParentXml(parsed);
}
else {
return parsed;
}
}
parseAndValidateXML(contents, path) {
try {
return this.parse(contents);
}
catch (e) {
// only attempt validating once there's an error to avoid the performance hit of validating every file
const validation = fast_xml_parser_1.XMLValidator.validate(contents);
if (validation !== true) {
throw new sfError_1.SfError(messages.getMessage('invalid_xml_parsing', [
path,
validation.err.msg,
validation.err.line.toString(),
validation.err.code,
]), 'LibraryError');
}
throw e;
}
}
getDecomposedChildren(dirPath) {
const children = [];
for (const fsPath of this.walk(dirPath)) {
const childXml = (0, path_1.parseMetadataXml)(fsPath);
const fileIsRootXml = childXml?.suffix === this.type.suffix || childXml?.suffix === this.type.legacySuffix;
if (childXml && !fileIsRootXml && this.type.children && childXml.suffix) {
const childTypeId = this.type.children?.suffixes[childXml.suffix];
const childType = this.type.children.types[childTypeId];
if (!childTypeId || !childType) {
void lifecycle_1.Lifecycle.getInstance().emitWarning(`${fsPath}: Expected a child type for ${childXml.suffix} in ${this.type.name} but none was found.`);
}
const childComponent = new SourceComponent({
name: childType?.suffix ? (0, path_1.baseWithoutSuffixes)(fsPath, childType) : (0, path_1.baseName)(fsPath),
type: this.type.children.types[childTypeId],
xml: fsPath,
parent: this,
}, this.treeContainer, this.forceIgnore);
children.push(childComponent);
}
}
return children;
}
// Get the children for non-decomposed types that have an xmlElementName
// and uniqueIdElement defined in the registry.
// E.g., CustomLabels, Workflows, SharingRules, AssignmentRules.
getNonDecomposedChildren() {
const parsed = this.parseXmlSync();
if (!this.type.children) {
throw new sfError_1.SfError(`There are no child types for ${this.type.name}`);
}
return Object.values(this.type.children.types).flatMap((childType) => {
const { uniqueIdElement, xmlElementName } = childType;
if (!uniqueIdElement || !xmlElementName) {
return [];
}
const xmlPathToChildren = `${this.type.name}.${xmlElementName}`;
const elements = (0, kit_1.ensureArray)((0, ts_types_1.get)(parsed, xmlPathToChildren, []));
return elements.map((element) => {
const name = (0, ts_types_1.getString)(element, uniqueIdElement);
if (!name) {
throw new sfError_1.SfError(`Missing ${uniqueIdElement} on ${childType.name} in ${this.xml ?? '<no xml>'}`);
}
return new SourceComponent({
name,
type: childType,
xml: this.xml,
parent: this,
}, this.treeContainer, this.forceIgnore);
});
});
}
*walk(fsPath) {
if (!this.treeContainer.isDirectory(fsPath)) {
yield fsPath;
}
else {
for (const child of this.treeContainer.readDirectory(fsPath)) {
const childPath = (0, node_path_1.join)(fsPath, child);
if (this.forceIgnore.denies(childPath)) {
continue;
}
else if (this.treeContainer.isDirectory(childPath)) {
yield* this.walk(childPath);
}
else {
yield childPath;
}
}
}
}
}
exports.SourceComponent = SourceComponent;
//# sourceMappingURL=sourceComponent.js.map