salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
394 lines (392 loc) • 23.1 kB
JavaScript
"use strict";
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceWorkspaceAdapter = void 0;
const util = require("util");
const path = require("path");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const core_1 = require("@salesforce/core");
const chalk = require("chalk");
const workspaceFileState_1 = require("./workspaceFileState");
const aggregateSourceElement_1 = require("./aggregateSourceElement");
const workspaceElement_1 = require("./workspaceElement");
const metadataTypeFactory_1 = require("./metadataTypeFactory");
const sourcePathStatusManager_1 = require("./sourcePathStatusManager");
const aggregateSourceElements_1 = require("./aggregateSourceElements");
const sourceLocations_1 = require("./sourceLocations");
/**
* private helper to log when a metadata type isn't supported
*
* @param {string} metadataName - the metadata type name
* @param {string} filePath - the filepath to the metadata item
* @private
*/
const _logUnsupported = function (metadataName, filePath) {
if (!util.isNullOrUndefined(filePath)) {
this.logger.warn(`Unsupported source member ${metadataName} at ${filePath}`);
}
else {
this.logger.warn(`Unsupported source member ${metadataName}`);
}
};
class SourceWorkspaceAdapter extends kit_1.AsyncCreatable {
/**
* @ignore
*/
constructor(options) {
super(options);
this.changedSourceElementsCache = new aggregateSourceElements_1.AggregateSourceElements();
this.allAggregateSourceElementsCache = new aggregateSourceElements_1.AggregateSourceElements();
this.options = options;
this.wsPath = options.org.config.getProjectPath();
if (kit_1.isEmpty(this.wsPath) || !ts_types_1.isString(this.wsPath)) {
throw core_1.SfdxError.create('salesforce-alm', 'source_workspace_adapter', 'missing_workspace');
}
// There appears to be difference between the parameter defaultPackagePath and the member instance defaultPackagePath
// It's reflected in the unit tests mdapiConvertApiTest.
// Since we are not doing strict null checking this runtime check is required for path.join.
if (kit_1.isEmpty(options.defaultPackagePath) || !ts_types_1.isString(options.defaultPackagePath)) {
throw core_1.SfdxError.create('salesforce-alm', 'source_workspace_adapter', 'missing_package_path');
}
this.isStateless = options.sourceMode === SourceWorkspaceAdapter.modes.STATELESS;
}
async init() {
this.logger = await core_1.Logger.child(this.constructor.name);
this.spsm = await sourcePathStatusManager_1.SourcePathStatusManager.create({
org: this.options.org,
isStateless: this.isStateless,
});
// If we only care about certain source paths (i.e., this.options.sourcePaths
// is defined) then initialize SourcePathStatusManager with just those paths.
let sourcePathInfos;
if (this.options.sourcePaths) {
sourcePathInfos = [];
for (let sourcePath of this.options.sourcePaths) {
sourcePath = path.resolve(sourcePath.trim());
const sourcePaths = await this.spsm.getSourcePathInfos({ sourcePath });
sourcePathInfos = [...sourcePathInfos, ...sourcePaths];
}
}
else {
sourcePathInfos = await this.spsm.getSourcePathInfos();
}
this.metadataRegistry = new this.options.metadataRegistryImpl(this.options.org);
this.fromConvert = this.options.fromConvert || false;
this.sourceLocations = await sourceLocations_1.SourceLocations.create({
metadataRegistry: this.metadataRegistry,
sourcePathInfos,
shouldBuildIndices: !this.fromConvert,
username: this.options.org.name,
});
this.wsPath = this.options.org.config.getProjectPath();
this.namespace = this.options.org.config.getAppConfig().namespace;
this.defaultSrcDir = path.join(this.wsPath, this.options.defaultPackagePath, 'main', 'default');
this.forceIgnore = this.spsm.forceIgnore;
this.defaultPackagePath = this.options.org.config.getAppConfig().defaultPackagePath;
this.pendingSourcePathInfos = new Map();
// Array of sourcePathInfos for directories
this.pendingDirectories = [];
this.logger.debug(`this.wsPath: ${this.wsPath}`);
this.logger.debug(`this.defaultPackagePath: ${this.defaultPackagePath}`);
this.logger.debug(`this.defaultSrcDir: ${this.defaultSrcDir}`);
this.logger.debug(`this.fromConvert: ${this.fromConvert}`);
this.logger.debug(`this.isStateless: ${this.isStateless}`);
// mdapi:convert and source:convert do not need to load changed aggregate source elements.
// The cache is specific to push, pull, status commands, but sourceWorkspaceAdapter is
// initialized for all source-related commands
if (!this.fromConvert) {
this.changedSourceElementsCache = await this.getAggregateSourceElements(true, null, true);
}
}
async revertSourcePathInfos() {
this.logger.debug('reverting source path infos');
await this.spsm.revert();
}
async backupSourcePathInfos() {
this.logger.debug('backup source path infos');
await this.spsm.backup();
}
updatePendingSourcePathInfos(change) {
const packageSourcePathInfos = this.pendingSourcePathInfos.get(change.package) || new Map();
const updated = packageSourcePathInfos.set(change.sourcePath, change);
this.pendingSourcePathInfos.set(change.package, updated);
}
getPendingSourcePathInfos(packageName) {
if (packageName) {
return Array.from(this.pendingSourcePathInfos.get(packageName).values());
}
else {
const elements = [];
this.pendingSourcePathInfos.forEach((sourceElements) => {
sourceElements.forEach((value) => elements.push(value));
});
return elements;
}
}
/**
* Get AggregateSourceElements (ASEs) in the workspace.
*
* To get all ASEs: SourceWorkspaceAdapter.getAggregateSourceElements(false);
* NOTE: This caches all ASEs so that subsequent calls do not incur this perf hit and just return the cache.
*
* To get all changed ASEs: SourceWorkspaceAdapter.getAggregateSourceElements(true);
*
* To get only ASEs from a certain path: SourceWorkspaceAdapter.getAggregateSourceElements(false, undefined, undefined, myPath);
*
* @param changesOnly - If true then return only the updated source elements (changed, new, or deleted)
* @param packageDirectory - the package directory from which to fetch source
* @param updatePendingPathInfos - the pending path infos should only be updated the first time this method is called
* in order to prevent subsequent calls from overwriting its values
* @param sourcePath the directory or file path specified for change-set development
* @returns - Map of aggregate source element key to aggregateSourceElement
* ex. { 'ApexClass__myApexClass' : aggregateSourceElement }
*/
async getAggregateSourceElements(changesOnly, packageDirectory, updatePendingPathInfos = false, sourcePath) {
if (!changesOnly && !this.allAggregateSourceElementsCache.isEmpty()) {
return this.allAggregateSourceElementsCache;
}
const aggregateSourceElementsByPkg = new aggregateSourceElements_1.AggregateSourceElements();
// Retrieve sourcePathInfos from the manager, filtered by what we want.
const pendingChanges = await this.spsm.getSourcePathInfos({
changesOnly,
packageDirectory,
sourcePath,
});
pendingChanges.forEach((change) => {
// Skip the directories
if (change.isDirectory) {
if (updatePendingPathInfos) {
this.pendingDirectories.push(change);
}
return;
}
if (kit_1.isEmpty(change.sourcePath) || !ts_types_1.isString(change.sourcePath)) {
throw core_1.SfdxError.create('salesforce-alm', 'source_workspace_adapter', 'invalid_source_path', [
change.sourcePath,
]);
}
const workspaceElementMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromSourcePath(change.sourcePath, this.metadataRegistry);
// If this isn't a source file skip it
if (!workspaceElementMetadataType) {
return;
}
// Does the metadata registry have this type blocklisted.
if (!this.metadataRegistry.isSupported(workspaceElementMetadataType.getMetadataName())) {
_logUnsupported.call(this, workspaceElementMetadataType.getMetadataName(), change.sourcePath);
return;
}
const aggregateFullName = workspaceElementMetadataType.getAggregateFullNameFromFilePath(change.sourcePath);
// In some cases getAggregateMetadataFilePathFromWorkspacePath is doing path.joins.. That requires null checking
// the sourcePath. We really really need strict null checking.
const aggregateMetadataFilePath = workspaceElementMetadataType.getAggregateMetadataFilePathFromWorkspacePath(change.sourcePath);
const aggregateMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getAggregateMetadataType(workspaceElementMetadataType.getAggregateMetadataName(), this.metadataRegistry);
const newAggregateSourceElement = new aggregateSourceElement_1.AggregateSourceElement(aggregateMetadataType, aggregateFullName, aggregateMetadataFilePath, this.metadataRegistry);
const workspaceFullName = workspaceElementMetadataType.getFullNameFromFilePath(change.sourcePath);
const deleteSupported = workspaceElementMetadataType.deleteSupported(workspaceFullName);
const workspaceElement = new workspaceElement_1.WorkspaceElement(workspaceElementMetadataType.getMetadataName(), workspaceFullName, change.sourcePath, change.state, deleteSupported);
const packageName = change.package;
const key = newAggregateSourceElement.getKey();
const aggregateSourceElement = aggregateSourceElementsByPkg.getSourceElement(packageName, key) || newAggregateSourceElement;
aggregateSourceElement.addWorkspaceElement(workspaceElement);
aggregateSourceElementsByPkg.setIn(packageName, key, aggregateSourceElement);
const deprecationMessage = aggregateMetadataType.getDeprecationMessage(aggregateFullName);
if (deprecationMessage && changesOnly) {
this.warnUser(undefined, deprecationMessage);
}
if (updatePendingPathInfos) {
this.updatePendingSourcePathInfos(change);
}
});
if (!changesOnly && !sourcePath) {
// We just created ASEs for all workspace elements so cache it.
this.allAggregateSourceElementsCache = aggregateSourceElementsByPkg;
}
return aggregateSourceElementsByPkg;
}
/**
* Commit pending changed file infos
*
* @returns {boolean} - Was the commit successful
*/
async commitPendingChanges(packageName) {
if (!this.pendingSourcePathInfos && !this.pendingDirectories) {
// getChanges or getAll must have been called prior to find the list
// of pending changes
return false;
}
let pendingChanges = [];
if (!util.isNullOrUndefined(this.pendingDirectories)) {
// be wary of directories that get deleted by cleanup methods
pendingChanges = pendingChanges.concat(this.pendingDirectories);
}
pendingChanges = pendingChanges.concat(this.getPendingSourcePathInfos(packageName));
if (pendingChanges.length > 0) {
this.logger.debug(`committing ${pendingChanges.length} pending changes`);
}
else {
this.logger.debug('no changes to commit');
}
await this.spsm.commitChangedPathInfos(pendingChanges);
return true;
}
/**
* Update the source stored in the workspace
*/
async updateSource(aggregateSourceElements, manifest, checkForDuplicates, unsupportedMimeTypes, forceoverwrite = false) {
let updatedPaths = [];
let deletedPaths = [];
this.logger.debug(`updateSource checkForDuplicates: ${checkForDuplicates}`);
for (const sourceElement of aggregateSourceElements.getAllSourceElements()) {
if (checkForDuplicates) {
sourceElement.checkForDuplicates();
}
const [newPathsForElements, updatedPathsForElements, deletedPathsForElements] = await sourceElement.commit(manifest, unsupportedMimeTypes, forceoverwrite);
updatedPaths = updatedPaths.concat(newPathsForElements, updatedPathsForElements);
deletedPaths = deletedPaths.concat(deletedPathsForElements);
}
this.logger.debug(`updateSource updatedPaths.length: ${updatedPaths.length}`);
this.logger.debug(`updateSource deletedPaths.length: ${deletedPaths.length}`);
await this.spsm.updateInfosForPaths(updatedPaths, deletedPaths);
return aggregateSourceElements;
}
/**
* Create a source element representation of a metadata change in the local workspace
*/
processMdapiFileProperty(changedSourceElements, retrieveRoot, fileProperty, bundleFileProperties) {
this.logger.debug(`processMdapiFileProperty retrieveRoot: ${retrieveRoot}`);
// Right now, all fileProperties returned by the mdapi are for aggregate metadata types
const aggregateMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromFileProperty(fileProperty, this.metadataRegistry);
const metadataName = aggregateMetadataType.getMetadataName();
const aggregateFullName = aggregateMetadataType.getAggregateFullNameFromFileProperty(fileProperty, this.namespace);
this.logger.debug(`processMdapiFileProperty aggregateFullName: ${aggregateFullName}`);
let aggregateMetadataPath;
if (!this.metadataRegistry.isSupported(metadataName)) {
return null;
}
// This searches the known metadata file paths on the local file system for one that matches our aggregateFullName
aggregateMetadataPath =
this.sourceLocations.getMetadataPath(metadataName, aggregateFullName) ||
this.sourceLocations.getFilePath(metadataName, aggregateFullName);
this.logger.debug(`processMdapiFileProperty aggregateMetadataPath: ${aggregateMetadataPath}`);
if (!!aggregateMetadataPath &&
this.fromConvert &&
!this.defaultSrcDir.includes(this.defaultPackagePath) &&
!aggregateMetadataPath.includes(this.defaultSrcDir)) {
// if a user specified a destination folder outside the default package directory and
// a file with same type and name exists but in a different pacakge directory then ignore it
aggregateMetadataPath = null;
}
const workspaceElementsToDelete = aggregateMetadataType.getWorkspaceElementsToDelete(aggregateMetadataPath, fileProperty);
// If the metadata path wasn't found we will use the default source directory
if (!aggregateMetadataPath) {
aggregateMetadataPath = aggregateMetadataType.getDefaultAggregateMetadataPath(aggregateFullName, this.defaultSrcDir, bundleFileProperties);
// Add the new path to the location mapping
this.sourceLocations.addMetadataPath(aggregateMetadataType.getAggregateMetadataName(), aggregateFullName, aggregateMetadataPath);
}
this.logger.debug(`processMdapiFileProperty aggregateMetadataPath: ${aggregateMetadataPath}`);
if (this.forceIgnore.accepts(aggregateMetadataPath)) {
this.logger.debug('processMdapiFileProperty this.forceIgnore.accepts(aggregateMetadataPath): true');
const newAggregateSourceElement = new aggregateSourceElement_1.AggregateSourceElement(aggregateMetadataType, aggregateFullName, aggregateMetadataPath, this.metadataRegistry);
const key = aggregateSourceElement_1.AggregateSourceElement.getKeyFromMetadataNameAndFullName(metadataName, aggregateFullName);
this.logger.debug(`processMdapiFilePropertykey: ${key}`);
const aggregateSourceElement = changedSourceElements.getSourceElement(newAggregateSourceElement.getPackageName(), key) ||
newAggregateSourceElement;
if (workspaceElementsToDelete.length > 0) {
workspaceElementsToDelete.forEach((deletedElement) => {
aggregateSourceElement.addPendingDeletedWorkspaceElement(deletedElement);
});
}
aggregateSourceElement.retrievedMetadataPath = aggregateMetadataType.getRetrievedMetadataPath(fileProperty, retrieveRoot, bundleFileProperties);
this.logger.debug(`processMdapiFileProperty aggregateSourceElement.retrievedMetadataPath: ${aggregateSourceElement.retrievedMetadataPath}`);
const retrievedContentPath = aggregateMetadataType.getRetrievedContentPath(fileProperty, retrieveRoot);
this.logger.debug(`retrievedContentPath: ${retrievedContentPath}`);
if (retrievedContentPath) {
if (!aggregateSourceElement.retrievedContentPaths) {
aggregateSourceElement.retrievedContentPaths = [];
}
aggregateSourceElement.retrievedContentPaths.push(retrievedContentPath);
}
changedSourceElements.setIn(aggregateSourceElement.getPackageName(), key, aggregateSourceElement);
return aggregateSourceElement;
}
return null;
}
/**
* Create a source element representation of a deleted metadata change in the local workspace
*
* @returns {AggregateSourceElement} - A source element or null if metadataType is not supported
*/
handleObsoleteSource(changedSourceElements, fullName, type) {
this.logger.debug(`handleObsoleteSource fullName: ${fullName}`);
const sourceMemberMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(type, this.metadataRegistry);
const aggregateFullName = sourceMemberMetadataType.getAggregateFullNameFromSourceMemberName(fullName);
this.logger.debug(`handleObsoleteSource aggregateFullName: ${aggregateFullName}`);
let metadataPath = this.sourceLocations.getMetadataPath(sourceMemberMetadataType.getAggregateMetadataName(), aggregateFullName);
if (!metadataPath) {
metadataPath = this.sourceLocations.getMetadataPath(type, aggregateFullName);
}
this.logger.debug(`handleObsoleteSource metadataPath: ${metadataPath}`);
if (metadataPath !== undefined) {
const key = aggregateSourceElement_1.AggregateSourceElement.getKeyFromMetadataNameAndFullName(sourceMemberMetadataType.getAggregateMetadataName(), aggregateFullName);
const packageName = core_1.SfdxProject.getInstance().getPackageNameFromPath(metadataPath);
this.logger.debug(`handleObsoleteSource key: ${key}, package: ${packageName}`);
let aggregateSourceElement = changedSourceElements.getSourceElement(packageName, key);
if (!aggregateSourceElement) {
const aggregateMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getAggregateMetadataType(sourceMemberMetadataType.getMetadataName(), this.metadataRegistry); // only used in one place ( to create AggregateSourceElement )
aggregateSourceElement = new aggregateSourceElement_1.AggregateSourceElement(aggregateMetadataType, aggregateFullName, metadataPath, this.metadataRegistry);
}
const shouldDeleteWorkspaceAggregate = sourceMemberMetadataType.shouldDeleteWorkspaceAggregate(type);
this.logger.debug(`shouldDeleteWorkspaceAggregate: ${shouldDeleteWorkspaceAggregate}`);
if (shouldDeleteWorkspaceAggregate) {
aggregateSourceElement.markForDelete();
}
else {
// the type is decomposed or AuraDefinition and we only want to delete components of an aggregate element
const sourcePathsToDelete = aggregateSourceElement.getWorkspacePathsForTypeAndFullName(sourceMemberMetadataType.getMetadataName(), fullName);
sourcePathsToDelete.forEach((sourcePathToDelete) => {
const deletedWorkspaceElement = new workspaceElement_1.WorkspaceElement(sourceMemberMetadataType.getMetadataName(), fullName, sourcePathToDelete, workspaceFileState_1.WorkspaceFileState.DELETED, true);
this.logger.debug(`add pending deleted workspace element: ${fullName}`);
aggregateSourceElement.addPendingDeletedWorkspaceElement(deletedWorkspaceElement);
});
}
changedSourceElements.setIn(aggregateSourceElement.packageName, key, aggregateSourceElement);
return aggregateSourceElement;
}
return null;
}
// Private to do move to UX.
warnUser(context, message) {
const warning = `${chalk.default.yellow('WARNING:')}`;
this.logger.warn(warning, message);
if (this.logger.shouldLog(core_1.LoggerLevel.WARN)) {
if (context && context.flags.json) {
if (!context.warnings) {
context.warnings = [];
}
context.warnings.push(message);
// Also log the message if valid stderr with json going to stdout.
if (kit_1.env.getBoolean('SFDX_JSON_TO_STDOUT', true)) {
console.error(warning, message); // eslint-disable-line no-console
}
}
else {
console.error(warning, message); // eslint-disable-line no-console
}
}
}
}
exports.SourceWorkspaceAdapter = SourceWorkspaceAdapter;
/**
* Adapter between scratch org source metadata and a local workspace
*/
// eslint-disable-next-line no-redeclare
(function (SourceWorkspaceAdapter) {
SourceWorkspaceAdapter.modes = { STATE: 0, STATELESS: 1 };
})(SourceWorkspaceAdapter = exports.SourceWorkspaceAdapter || (exports.SourceWorkspaceAdapter = {}));
//# sourceMappingURL=sourceWorkspaceAdapter.js.map