@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
390 lines • 24.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetadataResolver = 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 core_1 = require("@salesforce/core");
const path_1 = require("../utils/path");
const registryAccess_1 = require("../registry/registryAccess");
const constants_1 = require("../common/constants");
const sourceAdapterFactory_1 = require("./adapters/sourceAdapterFactory");
const forceIgnore_1 = require("./forceIgnore");
const treeContainers_1 = require("./treeContainers");
;
const messages = new core_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>"]]));
/**
* Resolver for metadata type and component objects.
*
* @internal
*/
class MetadataResolver {
registry;
tree;
useFsForceIgnore;
forceIgnoredPaths;
forceIgnore;
/**
* @param registry Custom registry data
* @param tree `TreeContainer` to traverse with
* @param useFsForceIgnore false = use default forceignore entries, true = search and use forceignore in project
*/
constructor(registry = new registryAccess_1.RegistryAccess(), tree = new treeContainers_1.NodeFSTreeContainer(), useFsForceIgnore = true) {
this.registry = registry;
this.tree = tree;
this.useFsForceIgnore = useFsForceIgnore;
this.forceIgnoredPaths = new Set();
}
/**
* Get the metadata component(s) from a file path.
*
* @param fsPath File path to metadata or directory
* @param inclusiveFilter Set to filter which components are resolved
*/
getComponentsFromPath(fsPath, inclusiveFilter) {
if (!this.tree.exists(fsPath)) {
throw new core_1.SfError(messages.getMessage('error_path_not_found', [fsPath]), 'TypeInferenceError');
}
// use the default ignore if we aren't using a real one
this.forceIgnore = this.useFsForceIgnore ? forceIgnore_1.ForceIgnore.findAndCreate(fsPath) : new forceIgnore_1.ForceIgnore();
if (this.tree.isDirectory(fsPath) && !resolveDirectoryAsComponent(this.registry)(this.tree)(fsPath)) {
return this.getComponentsFromPathRecursive(fsPath, inclusiveFilter);
}
const component = this.resolveComponent(fsPath, true);
return component ? [component] : [];
}
getComponentsFromPathRecursive(dir, inclusiveFilter) {
const dirQueue = [];
const components = [];
const ignore = new Set();
// don't apply forceignore rules against dirs
// `forceignore.denies` will pass a relative path to node-ignore, e.g.
// `path/to/force-app` -> `force-app`, note that there's no trailing slash
// so node-ignore will treat it as a file.
if (!this.tree.isDirectory(dir) && this.forceIgnore?.denies(dir)) {
return components;
}
for (const fsPath of this.tree
.readDirectory(dir)
.map((0, path_1.fnJoin)(dir))
// this method isn't truly recursive, we need to sort directories before files so we look as far down as possible
// before finding the parent and returning only it - by sorting, we make it as recursive as possible
.sort(this.sortDirsFirst)) {
if (ignore.has(fsPath)) {
continue;
}
if (this.tree.isDirectory(fsPath)) {
if (resolveDirectoryAsComponent(this.registry)(this.tree)(fsPath)) {
const component = this.resolveComponent(fsPath, true);
if (component && (!inclusiveFilter || inclusiveFilter.has(component))) {
components.push(component);
ignore.add(component.xml);
}
}
else {
dirQueue.push(fsPath);
}
}
else if (isMetadata(this.registry)(this.tree)(fsPath)) {
const component = this.resolveComponent(fsPath, false);
if (component) {
if (!inclusiveFilter || inclusiveFilter.has(component)) {
components.push(component);
ignore.add(component.content);
}
else {
for (const child of component.getChildren()) {
if (inclusiveFilter.has(child)) {
components.push(child);
}
}
}
// don't traverse further if not in a root type directory. performance optimization
// for mixed content types and ensures we don't add duplicates of the component.
const typeDir = (0, node_path_1.basename)((0, node_path_1.dirname)(component.type.inFolder ? (0, node_path_1.dirname)(fsPath) : fsPath));
if (component.type.strictDirectoryName && typeDir !== component.type.directoryName) {
return components;
}
}
}
}
return components.concat(dirQueue.flatMap((d) => this.getComponentsFromPathRecursive(d, inclusiveFilter)));
}
sortDirsFirst = (a, b) => {
if (this.tree.isDirectory(a) && this.tree.isDirectory(b)) {
return 0;
}
else if (this.tree.isDirectory(a) && !this.tree.isDirectory(b)) {
return -1;
}
else {
return 1;
}
};
resolveComponent(fsPath, isResolvingSource) {
if (this.forceIgnore?.denies(fsPath)) {
// don't resolve the component if the path is denied
this.forceIgnoredPaths.add(fsPath);
return;
}
const type = resolveType(this.registry)(this.tree)(fsPath);
if (type) {
const adapter = new sourceAdapterFactory_1.SourceAdapterFactory(this.registry, this.tree).getAdapter(type, this.forceIgnore);
// short circuit the component resolution unless this is a resolve for a
// source path or allowed content-only path, otherwise the adapter
// knows how to handle it
const shouldResolve = isResolvingSource ||
parseAsRootMetadataXml(fsPath) ||
!parseAsContentMetadataXml(this.registry)(fsPath) ||
!adapter.allowMetadataWithContent();
return shouldResolve ? adapter.getComponent(fsPath, isResolvingSource) : undefined;
}
if (isProbablyPackageManifest(this.tree)(fsPath))
return undefined;
void core_1.Lifecycle.getInstance().emitTelemetry({
eventName: 'metadata_resolver_type_inference_error',
library: 'SDR',
function: 'resolveComponent',
path: fsPath,
});
// The metadata type could not be inferred
// Attempt to guess the type and throw an error with actions
const actions = getSuggestionsForUnresolvedTypes(this.registry)(fsPath);
throw new core_1.SfError(messages.getMessage('error_could_not_infer_type', [fsPath]), 'TypeInferenceError', actions);
}
}
exports.MetadataResolver = MetadataResolver;
const isProbablyPackageManifest = (tree) => (fsPath) => {
// Perform some additional checks to see if this is a package manifest
if (fsPath.endsWith('.xml') && !fsPath.endsWith(constants_1.META_XML_SUFFIX)) {
// If it is named the default package.xml, assume it is a package manifest
if (fsPath.endsWith('package.xml'))
return true;
try {
// If the file contains the string "<Package xmlns", it is a package manifest
if (tree.readFileSync(fsPath).toString().includes('<Package xmlns'))
return true;
}
catch (err) {
const error = err;
if (error.message === 'Method not implemented') {
// Currently readFileSync is not implemented for zipTreeContainer
// Ignoring since this would have been ignored in the past
core_1.Logger.childFromRoot('metadataResolver.isProbablyPackageManifest').warn(`Type could not be inferred for ${fsPath}. It is likely this is a package manifest. Skipping...`);
return true;
}
return false;
}
}
return false;
};
/**
* Whether or not a directory that represents a single component should be resolved as one,
* or if it should be walked for additional components.
*
* If a type can be determined from a directory path, and the end part of the path isn't
* the directoryName of the type itself, infer the path is part of a mixedContent component
*
* @param registry the registry to resolve a type against
*/
const resolveDirectoryAsComponent = (registry) => (tree) => (dirPath) => {
const type = resolveType(registry)(tree)(dirPath);
if (type) {
const { directoryName, inFolder } = type;
const parts = dirPath.split(node_path_1.sep);
const folderOffset = inFolder ? 2 : 1;
const typeDirectoryIndex = parts.lastIndexOf(directoryName);
if (typeDirectoryIndex === -1 ||
parts.length - folderOffset <= typeDirectoryIndex ||
// ex: /lwc/folder/lwc/cmp
tree.readDirectory(dirPath).includes(type.directoryName) ||
// types with children may want to resolve them individually
type.children) {
return false;
}
}
else {
return false;
}
return true;
};
const isMetadata = (registry) => (tree) => (fsPath) => !!(0, path_1.parseMetadataXml)(fsPath) ||
parseAsContentMetadataXml(registry)(fsPath) ||
!!parseAsFolderMetadataXml(registry)(fsPath) ||
!!parseAsMetadata(registry)(tree)(fsPath);
/**
* Attempt to find similar types for types that could not be inferred
* To be used after executing the resolveType() method
*
* @returns an array of suggestions
* @param registry a metdata registry to resolve types against
*/
const getSuggestionsForUnresolvedTypes = (registry) => (fsPath) => {
const parsedMetaXml = (0, path_1.parseMetadataXml)(fsPath);
const metaSuffix = parsedMetaXml?.suffix;
// Finds close matches for meta suffixes
// Examples: https://regex101.com/r/vbRjwy/1
const closeMetaSuffix = new RegExp(/.+\.([^.-]+)(?:-.*)?\.xml/).exec((0, node_path_1.basename)(fsPath));
let guesses;
if (metaSuffix) {
guesses = registry.guessTypeBySuffix(metaSuffix);
}
else if (!metaSuffix && closeMetaSuffix) {
guesses = registry.guessTypeBySuffix(closeMetaSuffix[1]);
}
else {
guesses = registry.guessTypeBySuffix((0, path_1.extName)(fsPath));
}
// If guesses were found, format an array of strings to be passed to SfError's actions
return guesses && guesses.length > 0
? [
messages.getMessage('suggest_type_header', [(0, node_path_1.basename)(fsPath)]),
...guesses.map((guess) => messages.getMessage('suggest_type_did_you_mean', [
guess.suffixGuess,
typeof metaSuffix === 'string' || closeMetaSuffix ? '-meta.xml' : '',
guess.metadataTypeGuess.name,
])),
'', // A blank line makes this much easier to read (it doesn't seem to be possible to start a markdown message entry with a newline)
messages.getMessage('suggest_type_more_suggestions'),
]
: [];
};
// Get the array of directoryNames for types that have folderContentType
const getFolderContentTypeDirNames = (registry) => registry.getFolderContentTypes().map((t) => t.directoryName);
/**
* Identify metadata xml for a folder component:
* .../email/TestFolder-meta.xml
* .../reports/foo/bar-meta.xml
*
* Do not match this pattern:
* .../tabs/TestFolder.tab-meta.xml
*/
const parseAsFolderMetadataXml = (registry) => (fsPath) => {
let folderName;
const match = new RegExp(/(.+)-meta\.xml/).exec((0, node_path_1.basename)(fsPath));
if (match && !match[1].includes('.')) {
const parts = fsPath.split(node_path_1.sep);
if (parts.length > 1) {
const folderContentTypesDirs = getFolderContentTypeDirNames(registry);
// check if the path contains a folder content name as a directory
// e.g., `/reports/` and if it does return that folder name.
folderContentTypesDirs.some((dirName) => {
if (fsPath.includes(`${node_path_1.sep}${dirName}${node_path_1.sep}`)) {
folderName = dirName;
}
});
}
}
return folderName;
};
const resolveType = (registry) => (tree) => (fsPath) => {
// attempt 1 - check if the file is part of a component that requires a strict type folder
let resolvedType = resolveTypeFromStrictFolder(registry)(fsPath);
// attempt 2 - check if it's a metadata xml file
if (!resolvedType) {
const parsedMetaXml = (0, path_1.parseMetadataXml)(fsPath);
if (parsedMetaXml?.suffix) {
resolvedType = registry.getTypeBySuffix(parsedMetaXml.suffix);
}
}
// attempt 2.5 - test for a folder style xml file
if (!resolvedType) {
const metadataFolder = parseAsFolderMetadataXml(registry)(fsPath);
if (metadataFolder) {
// multiple matching directories may exist - folder components are not 'inFolder'
resolvedType = registry.findType((type) => type.directoryName === metadataFolder && !type.inFolder);
}
}
// attempt 3 - try treating the file extension name as a suffix
if (!resolvedType) {
resolvedType = registry.getTypeBySuffix((0, path_1.extName)(fsPath));
// Metadata types with `strictDirectoryName` should have been caught in "attempt 1".
// If the metadata returned from this lookup has a `strictDirectoryName`, something is wrong.
// It is likely that the metadata file is misspelled or has the wrong suffix.
// A common occurrence is that a misspelled metadata file will fall back to
// `EmailServicesFunction` because that is the default for the `.xml` suffix
if (resolvedType?.strictDirectoryName === true) {
resolvedType = undefined;
}
}
// attempt 4 - try treating the content as metadata
if (!resolvedType) {
const metadata = parseAsMetadata(registry)(tree)(fsPath);
if (metadata) {
resolvedType = registry.getTypeByName(metadata);
}
}
return resolvedType;
};
/**
* Any file with a registered suffix is potentially a content metadata file.
*
* @param registry a metadata registry to resolve types agsinst
*/
const parseAsContentMetadataXml = (registry) => (fsPath) => {
const suffixType = registry.getTypeBySuffix((0, path_1.extName)(fsPath));
if (!suffixType)
return false;
const matchesSuffixType = fsPath.split(node_path_1.sep).includes(suffixType.directoryName);
if (matchesSuffixType)
return matchesSuffixType;
// at this point, the suffixType is not a match, so check for strict folder types
return !!resolveTypeFromStrictFolder(registry)(fsPath);
};
/**
* If this file should be considered as a metadata file then return the metadata type
*/
const parseAsMetadata = (registry) => (tree) => (fsPath) => {
if (tree.isDirectory(fsPath)) {
return;
}
return ['DigitalExperience', 'ExperiencePropertyTypeBundle', 'LightningTypeBundle', 'ContentTypeBundle']
.map((type) => registry.getTypeByName(type))
.find((type) => fsPath.split(node_path_1.sep).includes(type.directoryName))?.name;
};
const resolveTypeFromStrictFolder = (registry) => (fsPath) => {
const pathParts = fsPath.split(node_path_1.sep);
// first, filter out types that don't appear in the path
// then iterate using for/of to allow for early break
return registry
.getStrictFolderTypes()
.filter(pathIncludesDirName(pathParts)) // the type's directory is in the path
.filter(folderTypeFilter(fsPath))
.find((type) =>
// any of the following options is considered a good match
isMixedContentOrBundle(type) ||
suffixMatches(type, fsPath) ||
childSuffixMatches(type, fsPath) ||
legacySuffixMatches(type, fsPath));
};
/** the type has children and the file suffix (in source format) matches a child type suffix of the type we think it is */
const childSuffixMatches = (type, fsPath) => Object.values(type.children?.types ?? {}).some((childType) => suffixMatches(childType, fsPath) || legacySuffixMatches(childType, fsPath));
/** the file suffix (in source or mdapi format) matches the type suffix we think it is */
const suffixMatches = (type, fsPath) => typeof type.suffix === 'string' &&
(fsPath.endsWith(type.suffix) || fsPath.endsWith(appendMetaXmlSuffix(type.suffix)));
const legacySuffixMatches = (type, fsPath) => {
if (typeof type.legacySuffix === 'string' &&
(fsPath.endsWith(type.legacySuffix) || fsPath.endsWith(appendMetaXmlSuffix(type.legacySuffix)))) {
void core_1.Lifecycle.getInstance().emitWarning(`The ${type.name} component at ${fsPath} uses the legacy suffix ${type.legacySuffix}. This suffix is deprecated and will be removed in a future release.`);
return true;
}
return false;
};
const appendMetaXmlSuffix = (suffix) => `${suffix}${constants_1.META_XML_SUFFIX}`;
const isMixedContentOrBundle = (type) => typeof type.strategies?.adapter === 'string' && ['mixedContent', 'bundle'].includes(type.strategies.adapter);
/** types with folders only have folder components living at the top level.
* if the fsPath is a folder component, let a future strategy deal with it
*/
const folderTypeFilter = (fsPath) => (type) => !type.inFolder || (0, path_1.parentName)(fsPath) !== type.directoryName;
const pathIncludesDirName = (parts) => (type) => parts.includes(type.directoryName);
/**
* Any metadata xml file (-meta.xml) is potentially a root metadata file.
*
* @param fsPath File path of a potential metadata xml file
*/
const parseAsRootMetadataXml = (fsPath) => Boolean((0, path_1.parseMetadataXml)(fsPath));
//# sourceMappingURL=metadataResolver.js.map
;