@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
179 lines • 8.25 kB
JavaScript
;
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.shouldConvertPaths = exports.toKey = exports.isComponentNotFoundWarningMessage = exports.getState = exports.getDeployMessages = exports.createResponses = exports.sanitizeDeployMessage = void 0;
const node_path_1 = require("node:path");
const sfError_1 = require("@salesforce/core/sfError");
const kit_1 = require("@salesforce/kit");
const registry_1 = require("../registry/registry");
const types_1 = require("./types");
const diagnosticUtil_1 = require("./diagnosticUtil");
const utils_1 = require("./utils");
/**
* Fix any issues with the deploy message returned by the api.
* TODO: remove cases if fixes are made in the api.
*/
const sanitizeDeployMessage = (message) => {
if (!hasComponentType(message)) {
throw new sfError_1.SfError(`Missing componentType in deploy message ${message.fullName} ${message.fileName}`);
}
// mdapi error messages have the type as "FooSettings" but SDR only recognizes "Settings"
if (message.componentType.endsWith('Settings') && message.fileName.endsWith('.settings')) {
return {
...message,
componentType: 'Settings',
};
}
if (message.componentType === registry_1.registry.types.lightningcomponentbundle.name) {
return {
...message,
fullName: message.fullName.replace(/markup:\/\/[a-z|0-9|_]+:/i, ''),
};
}
if (message.componentType === registry_1.registry.types.document.name) {
return {
...message,
// strip document extension from fullName
fullName: (0, node_path_1.join)((0, node_path_1.dirname)(message.fullName), (0, node_path_1.basename)(message.fullName, (0, node_path_1.extname)(message.fullName))),
};
}
// Treat emailTemplateFolder as EmailFolder
if (message.componentType === registry_1.registry.types.emailtemplatefolder.name) {
return {
...message,
// strip document extension from fullName
componentType: registry_1.registry.types.emailfolder.name,
};
}
return message;
};
exports.sanitizeDeployMessage = sanitizeDeployMessage;
// components with children are already taken care of through the messages, so don't walk their content directories.
const shouldWalkContent = (component) => typeof component.content === 'string' &&
(!component.type.children ||
Object.values(component.type.children.types).some((t) => t.unaddressableWithoutParent === true || t.isAddressable === false));
const createResponses = (projectPath) => (component, responseMessages) => responseMessages.flatMap((message) => {
const state = (0, exports.getState)(message);
const base = { fullName: component.fullName, type: component.type.name };
if (state === types_1.ComponentStatus.Failed) {
return [{ ...base, state, ...(0, diagnosticUtil_1.parseDeployDiagnostic)(component, message) }];
}
return ((0, utils_1.isWebAppBundle)(component)
? [
{
...base,
state,
filePath: component.content,
},
...component.walkContent().map((filePath) => ({
fullName: getWebAppBundleContentFullName(component)(filePath),
type: 'DigitalExperience',
state,
filePath,
})),
]
: [
...(shouldWalkContent(component)
? component.walkContent().map((filePath) => ({ ...base, state, filePath }))
: []),
...(component.xml ? [{ ...base, state, filePath: component.xml }] : []),
]).map((response) => ({
...response,
filePath:
// deployResults will produce filePaths relative to cwd, which might not be set in all environments
// if our CS had a projectDir set, we'll make the results relative to that path unless it already is
projectPath && process.cwd() !== projectPath && !response.filePath.startsWith(projectPath)
? (0, node_path_1.join)(projectPath, response.filePath)
: response.filePath,
}));
});
exports.createResponses = createResponses;
const getWebAppBundleContentFullName = (component) => (filePath) => {
// Normalize paths to ensure relative() works correctly on Windows
const normalizedContent = component.content.split(node_path_1.sep).join(node_path_1.posix.sep);
const normalizedFilePath = filePath.split(node_path_1.sep).join(node_path_1.posix.sep);
const relPath = node_path_1.posix.relative(normalizedContent, normalizedFilePath);
return node_path_1.posix.join(component.fullName, relPath);
};
/**
* Groups messages from the deploy result by component fullName and type
*/
const getDeployMessages = (result) => {
const messageMap = new Map();
const failedComponentKeys = new Set();
const failureMessages = (0, kit_1.ensureArray)(result.details.componentFailures);
const successMessages = (0, kit_1.ensureArray)(result.details.componentSuccesses);
for (const failure of failureMessages) {
const sanitized = (0, exports.sanitizeDeployMessage)(failure);
const componentLike = {
fullName: sanitized.fullName,
type: sanitized.componentType,
};
const key = (0, exports.toKey)(componentLike);
if (!messageMap.has(key)) {
messageMap.set(key, []);
}
messageMap.get(key)?.push(sanitized);
failedComponentKeys.add(key);
}
for (const success of successMessages) {
const sanitized = (0, exports.sanitizeDeployMessage)(success);
const componentLike = {
fullName: sanitized.fullName,
type: sanitized.componentType,
};
const key = (0, exports.toKey)(componentLike);
// this will ensure successes aren't reported if there is a failure for
// the same component. e.g. lwc returns failures and successes
if (!failedComponentKeys.has(key)) {
messageMap.set(key, [sanitized]);
}
}
return messageMap;
};
exports.getDeployMessages = getDeployMessages;
const getState = (message) => {
if (isTrue(message.created)) {
return types_1.ComponentStatus.Created;
}
else if (isTrue(message.changed)) {
return types_1.ComponentStatus.Changed;
}
else if (isTrue(message.deleted)) {
return types_1.ComponentStatus.Deleted;
}
else if (!isTrue(message.success)) {
return types_1.ComponentStatus.Failed;
}
return types_1.ComponentStatus.Unchanged;
};
exports.getState = getState;
/* Type guard for asserting that a DeployMessages has a componentType, problem, and problemType === Warning*/
const isComponentNotFoundWarningMessage = (message) => hasComponentType(message) &&
message.problemType === 'Warning' &&
typeof message.problem === 'string' &&
message.problem?.startsWith(`No ${message.componentType} named: `);
exports.isComponentNotFoundWarningMessage = isComponentNotFoundWarningMessage;
const hasComponentType = (message) => typeof message.componentType === 'string';
const toKey = (component) => {
const type = typeof component.type === 'string' ? component.type : component.type.name;
return `${type}#${exports.shouldConvertPaths ? component.fullName.split(node_path_1.sep).join(node_path_1.posix.sep) : component.fullName}`;
};
exports.toKey = toKey;
const isTrue = (value) => value === 'true' || value === true;
exports.shouldConvertPaths = node_path_1.sep !== node_path_1.posix.sep;
//# sourceMappingURL=deployMessages.js.map