@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
276 lines • 18.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isWebAppBaseType = exports.DigitalExperienceSourceAdapter = void 0;
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const node_path_1 = require("node:path");
const messages_1 = require("@salesforce/core/messages");
const ts_types_1 = require("@salesforce/ts-types");
const constants_1 = require("../../common/constants");
const sourceComponent_1 = require("../sourceComponent");
const path_1 = require("../../utils/path");
const bundleSourceAdapter_1 = require("./bundleSourceAdapter");
;
const messages = new messages_1.Messages('@salesforce/source-deploy-retrieve', 'sdr', new Map([["md_request_fail", "Metadata API request failed: %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_invalid_test_level", "TestLevel cannot be '%s' unless API version is %s or later"], ["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>"]]));
// Constants for DigitalExperience base types
const WEB_APP_BASE_TYPE = 'web_app';
/**
* Source Adapter for DigitalExperience metadata types. This metadata type is a bundled type of the format
*
* __Example Structure__:
*
*```text
* site/
* ├── foos/
* | ├── sfdc_cms__appPage/
* | | ├── mainAppPage/
* | | | ├── _meta.json
* | | | ├── content.json
* | ├── sfdc_cms__view/
* | | ├── view1/
* | | | ├── _meta.json
* | | | ├── content.json
* | | | ├── fr.json
* | | | ├── en.json
* | | ├── view2/
* | | | ├── _meta.json
* | | | ├── content.json
* | | | ├── ar.json
* | | ├── view3/
* | | | ├── _meta.json
* | | | ├── content.json
* | | | ├── mobile/
* | | | | ├──mobile.json
* | | | ├── tablet/
* | | | | ├──tablet.json
* | ├── foos.digitalExperience-meta.xml
* content/
* ├── bars/
* | ├── bars.digitalExperience-meta.xml
* web_app/
* ├── zenith/
* | ├── css/
* | | ├── header/
* | | | ├── header.css
* | | ├── home.css
* | ├── js/
* | | ├── home.js
* | ├── html/
* | | ├── home.html
* | ├── images/
* | | ├── logos/
* | | | ├── logo.png
* ```
*
* In the above structure the metadata xml file ending with "digitalExperience-meta.xml" belongs to DigitalExperienceBundle MD type.
* The "_meta.json" files are child metadata files of DigitalExperienceBundle belonging to DigitalExperience MD type. The rest of the files in the
* corresponding folder are the contents to the DigitalExperience metadata. So, incase of DigitalExperience the metadata file is a JSON file
* and not an XML file.
*
* For web_app base type, the bundle is identified by directory structure alone without metadata XML files.
*/
class DigitalExperienceSourceAdapter extends bundleSourceAdapter_1.BundleSourceAdapter {
getComponent(path, isResolvingSource = true) {
if (this.isBundleType() && (0, exports.isWebAppBaseType)(path) && this.tree.isDirectory(path)) {
const pathParts = path.split(node_path_1.sep);
const bundleNameIndex = getDigitalExperiencesIndex(path) + 2;
if (bundleNameIndex === pathParts.length - 1) {
return this.populate(path, undefined);
}
}
return super.getComponent(path, isResolvingSource);
}
parseAsRootMetadataXml(path) {
if ((0, exports.isWebAppBaseType)(path)) {
return undefined;
}
if (!this.isBundleType() && !path.endsWith(this.type.metaFileSuffix ?? '_meta.json')) {
return undefined;
}
return super.parseAsRootMetadataXml(path);
}
getRootMetadataXmlPath(trigger) {
if (this.isBundleType()) {
return this.getBundleMetadataXmlPath(trigger);
}
if ((0, exports.isWebAppBaseType)(trigger)) {
return '';
}
// metafile name = metaFileSuffix for DigitalExperience.
if (!this.type.metaFileSuffix) {
throw messages.createError('missingMetaFileSuffix', [this.type.name]);
}
return (0, node_path_1.join)((0, node_path_1.dirname)(trigger), this.type.metaFileSuffix);
}
trimPathToContent(path) {
if (this.isBundleType()) {
return path;
}
if ((0, exports.isWebAppBaseType)(path)) {
// For web_app, trim to the bundle directory: digitalExperiences/web_app/WebApp
return getWebAppBundleDir(path);
}
const pathToContent = (0, node_path_1.dirname)(path);
const parts = pathToContent.split(node_path_1.sep);
/* Handle mobile or tablet variants.Eg- digitalExperiences/site/lwr11/sfdc_cms__view/home/mobile/mobile.json
or inline media files where files can be in any subdiretory. Eg - digitalExperiences/site/lwr11/sfdc_cms__lwc/localComp/folder1/foler1_1/localCompHelper.html
from the digitalExperience folder go till we find the ContentApiName folder
*/
const digitalExperiencesIndex = parts.indexOf('digitalExperiences');
if (digitalExperiencesIndex > -1) {
const digitalExperiencesLength = digitalExperiencesIndex + 1;
const contentFolderLength = digitalExperiencesLength + contentParts.length;
if (parts.length > contentFolderLength) {
parts.length = contentFolderLength;
return parts.join(node_path_1.sep);
}
}
return pathToContent;
}
populate(trigger, component) {
if (this.isBundleType() && component) {
// for top level types we don't need to resolve parent
return component;
}
if ((0, exports.isWebAppBaseType)(trigger)) {
return this.populateWebAppBundle(trigger, component);
}
const source = super.populate(trigger, component);
const parentType = this.registry.getParentType(this.type.id);
// we expect source, parentType and content to be defined.
if (!source || !parentType || !source.content) {
throw messages.createError('error_failed_convert', [component?.fullName ?? this.type.name]);
}
const parent = new sourceComponent_1.SourceComponent({
name: this.getBundleName(source.content),
type: parentType,
xml: this.getBundleMetadataXmlPath(source.content),
}, this.tree, this.forceIgnore);
return new sourceComponent_1.SourceComponent({
name: calculateNameFromPath(source.content),
type: this.type,
content: source.content,
xml: source.xml,
parent,
parentType,
}, this.tree, this.forceIgnore);
}
parseMetadataXml(path) {
const xml = super.parseMetadataXml(path);
if (xml && this.isBundleType()) {
return {
fullName: this.getBundleName(path),
suffix: xml.suffix,
path: xml.path,
};
}
}
populateWebAppBundle(trigger, component) {
if (component) {
return component;
}
const pathParts = trigger.split(node_path_1.sep);
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
// Extract bundle name: web_app/WebApp3 (always use posix separator for metadata names)
const baseType = pathParts[digitalExperiencesIndex + 1];
const spaceApiName = pathParts[digitalExperiencesIndex + 2];
const bundleName = [baseType, spaceApiName].join('/');
// Extract bundle directory: /path/to/digitalExperiences/web_app/WebApp3
const bundleDir = getWebAppBundleDir(trigger);
// Get the DigitalExperienceBundle type
const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id);
if (!parentType) {
throw messages.createError('error_failed_convert', [bundleName]);
}
return new sourceComponent_1.SourceComponent({
name: bundleName,
type: parentType,
content: bundleDir,
}, this.tree, this.forceIgnore);
}
getBundleName(contentPath) {
if ((0, exports.isWebAppBaseType)(contentPath)) {
const pathParts = contentPath.split(node_path_1.sep);
const digitalExperiencesIndex = getDigitalExperiencesIndex(contentPath);
const baseType = pathParts[digitalExperiencesIndex + 1];
const spaceApiName = pathParts[digitalExperiencesIndex + 2];
return [baseType, spaceApiName].join('/');
}
const bundlePath = this.getBundleMetadataXmlPath(contentPath);
return [(0, path_1.parentName)((0, node_path_1.dirname)(bundlePath)), (0, path_1.parentName)(bundlePath)].join('/');
}
getBundleMetadataXmlPath(path) {
if (this.isBundleType() && path.endsWith(constants_1.META_XML_SUFFIX)) {
// if this is the bundle type and it ends with -meta.xml, then this is the bundle metadata xml path
return path;
}
if ((0, exports.isWebAppBaseType)(path)) {
return '';
}
const pathParts = path.split(node_path_1.sep);
const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName);
// 3 because we want 'digitalExperiences' directory, 'baseType' directory and 'bundleName' directory
const basePath = pathParts.slice(0, typeFolderIndex + 3).join(node_path_1.sep);
const bundleFileName = pathParts[typeFolderIndex + 2];
const suffix = (0, ts_types_1.ensureString)(this.isBundleType() ? this.type.suffix : this.registry.getParentType(this.type.id)?.suffix);
return `${basePath}${node_path_1.sep}${bundleFileName}.${suffix}${constants_1.META_XML_SUFFIX}`;
}
isBundleType() {
return this.type.id === 'digitalexperiencebundle';
}
}
exports.DigitalExperienceSourceAdapter = DigitalExperienceSourceAdapter;
/**
* @param contentPath This hook is called only after trimPathToContent() is called. so this will always be a folder structure
* @returns name of type/apiName format
*/
const calculateNameFromPath = (contentPath) => `${(0, path_1.parentName)(contentPath)}/${(0, path_1.baseName)(contentPath)}`;
const digitalExperienceStructure = (0, node_path_1.join)('BaseType', 'SpaceApiName', 'ContentType', 'ContentApiName');
const contentParts = digitalExperienceStructure.split(node_path_1.sep);
/**
* Checks if the given path belongs to the web_app base type.
* web_app base type has a simpler structure without ContentType folders.
* Structure: digitalExperiences/web_app/spaceApiName/...files...
*/
const isWebAppBaseType = (path) => {
const pathParts = path.split(node_path_1.sep);
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
return pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE;
};
exports.isWebAppBaseType = isWebAppBaseType;
/**
* Gets the digitalExperiences index from a path.
* Returns -1 if not found.
*/
const getDigitalExperiencesIndex = (path) => {
const pathParts = path.split(node_path_1.sep);
return pathParts.indexOf('digitalExperiences');
};
/**
* Gets the web_app bundle directory path.
* For a path like: /path/to/digitalExperiences/web_app/WebApp/src/App.js
* Returns: /path/to/digitalExperiences/web_app/WebApp
*/
const getWebAppBundleDir = (path) => {
const pathParts = path.split(node_path_1.sep);
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
if (digitalExperiencesIndex > -1 && pathParts.length > digitalExperiencesIndex + 3) {
// Return up to digitalExperiences/web_app/spaceApiName
return pathParts.slice(0, digitalExperiencesIndex + 3).join(node_path_1.sep);
}
return path;
};
//# sourceMappingURL=digitalExperienceSourceAdapter.js.map