UNPKG

@salesforce/source-deploy-retrieve

Version:

JavaScript library to run Salesforce metadata deploys and retrieves

390 lines 24.4 kB
"use strict"; 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