UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

394 lines (392 loc) 23.1 kB
"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