@salesforce/source-deploy-retrieve
Version:
JavaScript library to run Salesforce metadata deploys and retrieves
156 lines • 8.73 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPackageOptions = exports.extract = void 0;
/*
* Copyright (c) 2023, 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 path = __importStar(require("node:path"));
const ts_types_1 = require("@salesforce/ts-types");
const graceful_fs_1 = __importDefault(require("graceful-fs"));
const metadataConverter_1 = require("../convert/metadataConverter");
const componentSet_1 = require("../collections/componentSet");
const treeContainers_1 = require("../resolve/treeContainers");
const path_1 = require("../utils/path");
const types_1 = require("./types");
const extract = async ({ zip, options, logger, mainComponents, }) => {
const components = [];
const { merge, output, registry } = options;
const converter = new metadataConverter_1.MetadataConverter(registry);
const tree = await treeContainers_1.ZipTreeContainer.create(zip);
const partialDeleteFileResponses = [];
const packages = [
{ zipTreeLocation: 'unpackaged', outputDir: output },
...(0, exports.getPackageOptions)(options.packageOptions).map(({ name, outputDir }) => ({
zipTreeLocation: name,
outputDir,
})),
];
for (const pkg of packages) {
const outputConfig = merge
? {
type: 'merge',
mergeWith: mainComponents?.getSourceComponents() ?? [],
defaultDirectory: pkg.outputDir,
forceIgnoredPaths: mainComponents?.forceIgnoredPaths ?? new Set(),
}
: {
type: 'directory',
outputDirectory: pkg.outputDir,
};
const retrievedComponents = componentSet_1.ComponentSet.fromSource({
fsPaths: [pkg.zipTreeLocation],
registry,
tree,
})
.getSourceComponents()
.toArray();
if (merge) {
partialDeleteFileResponses.push(...handlePartialDeleteMerges({ retrievedComponents, tree, mainComponents, logger }));
}
// this is intentional sequential
// eslint-disable-next-line no-await-in-loop
const convertResult = await converter.convert(retrievedComponents, 'source', outputConfig);
components.push(...(convertResult?.converted ?? []));
// additional partialDelete logic for decomposed types are handled in the transformer
partialDeleteFileResponses.push(...(convertResult?.deleted ?? []));
}
return { componentSet: new componentSet_1.ComponentSet(components, registry), partialDeleteFileResponses };
};
exports.extract = extract;
const getPackageOptions = (packageOptions) => (packageOptions ?? []).map((po) => (0, ts_types_1.isString)(po) ? { name: po, outputDir: po } : { name: po.name, outputDir: po.outputDir ?? po.name });
exports.getPackageOptions = getPackageOptions;
// Some bundle-like components can be partially deleted in the org, then retrieved. When this
// happens, the deleted files need to be deleted on the file system and added to the FileResponses
// that are returned by `RetrieveResult.getFileResponses()` for accuracy. The component types that
// support this behavior are defined in the metadata registry with `"supportsPartialDelete": true`.
// However, not all types can be partially deleted in the org. Currently this only applies to
// DigitalExperienceBundle and ExperienceBundle.
// side effect: deletes files
const handlePartialDeleteMerges = ({ mainComponents, retrievedComponents, tree, logger, }) => {
// Find all merge (local) components that support partial delete.
const partialDeleteComponents = new Map((mainComponents?.getSourceComponents().toArray() ?? [])
.filter(supportsPartialDeleteAndHasContent)
.map((comp) => [comp.fullName, { contentPath: comp.content, contentList: graceful_fs_1.default.readdirSync(comp.content) }]));
// Compare the contents of the retrieved components that support partial delete with the
// matching merge components. If the merge components have files that the retrieved components
// don't, delete the merge component and add all locally deleted files to the partial delete list
// so that they are added to the `FileResponses` as deletes.
return partialDeleteComponents.size === 0
? [] // If no partial delete components were in the mergeWith ComponentSet, no need to continue.
: retrievedComponents
.filter(supportsPartialDeleteAndIsInMap(partialDeleteComponents))
.filter((comp) => partialDeleteComponents.get(comp.fullName)?.contentPath)
.filter(supportsPartialDeleteAndHasZipContent(tree))
.flatMap((comp) => {
// asserted to be defined by the filter above
const matchingLocalComp = partialDeleteComponents.get(comp.fullName);
const remoteContentList = new Set(tree.readDirectory(comp.content));
return matchingLocalComp.contentList
.filter((fileName) => !remoteContentList.has(fileName))
.filter((fileName) => !pathOrSomeChildIsIgnored(logger)(comp)(matchingLocalComp)(fileName))
.map((fileName) => ({
fullName: comp.fullName,
type: comp.type.name,
state: types_1.ComponentStatus.Deleted,
filePath: path.join(matchingLocalComp.contentPath, fileName),
}))
.map(deleteFilePath(logger));
});
};
const supportsPartialDeleteAndHasContent = (comp) => supportsPartialDelete(comp) && typeof comp.content === 'string' && graceful_fs_1.default.statSync(comp.content).isDirectory();
const supportsPartialDeleteAndHasZipContent = (tree) => (comp) => supportsPartialDelete(comp) && typeof comp.content === 'string' && tree.isDirectory(comp.content);
const supportsPartialDeleteAndIsInMap = (partialDeleteComponents) => (comp) => supportsPartialDelete(comp) && partialDeleteComponents.has(comp.fullName);
const supportsPartialDelete = (comp) => comp.type.supportsPartialDelete === true;
// If fileName is forceignored it is not counted as a diff. If fileName is a directory
// we have to read the contents to check forceignore status or we might get a false
// negative with `denies()` due to how the ignore library works.
const pathOrSomeChildIsIgnored = (logger) => (component) => (localComp) => (fileName) => {
const fileNameFullPath = path.join(localComp.contentPath, fileName);
return graceful_fs_1.default.statSync(fileNameFullPath).isDirectory()
? graceful_fs_1.default.readdirSync(fileNameFullPath).map((0, path_1.fnJoin)(fileNameFullPath)).some(isForceIgnored(logger)(component))
: isForceIgnored(logger)(component)(fileNameFullPath);
};
const isForceIgnored = (logger) => (comp) => (filePath) => {
const ignored = comp.getForceIgnore().denies(filePath);
if (ignored) {
logger.debug(`Local component has ${filePath} while remote does not, but it is forceignored so ignoring.`);
}
return ignored;
};
const deleteFilePath = (logger) => (fr) => {
if (fr.filePath) {
logger.debug(`Local component (${fr.fullName}) contains ${fr.filePath} while remote component does not. This file is being removed.`);
graceful_fs_1.default.rmSync(fr.filePath, { recursive: true, force: true });
}
return fr;
};
//# sourceMappingURL=retrieveExtract.js.map
;