UNPKG

salesforce-alm

Version:

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

459 lines (457 loc) 21.8 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 */ const path = require("path"); const fsx = require("fs-extra"); const klaw = require("klaw"); const BBPromise = require("bluebird"); const optional = require("optional-js"); const _ = require("lodash"); const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve"); const core_1 = require("@salesforce/core"); const Force = require("../core/force"); const almError = require("../core/almError"); const logger = require("../core/logApi"); const glob = BBPromise.promisify(require('glob')); const Messages = require("../messages"); const waveTemplateBundleMetadataType_1 = require("./metadataTypeImpl/waveTemplateBundleMetadataType"); const auraDefinitionBundleMetadataType_1 = require("./metadataTypeImpl/auraDefinitionBundleMetadataType"); const MetadataRegistry = require("./metadataRegistry"); const messages = Messages(); const workspaceFileState_1 = require("./workspaceFileState"); const parseManifestEntriesArray_1 = require("./parseManifestEntriesArray"); const metadataTypeFactory_1 = require("./metadataTypeFactory"); const lightningComponentBundleMetadataType_1 = require("./metadataTypeImpl/lightningComponentBundleMetadataType"); const sourceWorkspaceAdapter_1 = require("./sourceWorkspaceAdapter"); const sourcePathUtil_1 = require("./sourcePathUtil"); const aggregateSourceElements_1 = require("./aggregateSourceElements"); const fsx_ensureDir = BBPromise.promisify(fsx.ensureDir); /** * Helper to normalize a path * * @param {string} targetValue - the raw path * @returns {string} - a trimmed path without a trailing slash. * @private */ const _normalizePath = function (targetValue) { let localTargetValue = targetValue.trim(); if (localTargetValue.endsWith(path.sep)) { localTargetValue = localTargetValue.substr(0, localTargetValue.length - 1); } return path.resolve(localTargetValue); }; /** * Process a file from the metadata package. * * @param {object} typeDef - object type from the metadata registry * @param {string} pathWithPackage - the filepath including the metadata package * @param {string} fullName - the computed full name @see _getFullName * @param sourceWorkspaceAdapter - the org/source workspace adapter * @param {AggregateSourceElements} sourceElements - accumulator of created AggregateSourceElements * @private */ const _processFile = function (metadataType, pathWithPackage, fullName, sourceWorkspaceAdapter, sourceElements) { const fileProperties = { type: metadataType.getMetadataName(), fileName: pathWithPackage, fullName, }; const retrieveRoot = path.join(this._package_root, '..'); // Each Aura bundle has a definition file that has one of the suffixes: .app, .cmp, .design, .evt, etc. // In order to associate each sub-component of an aura bundle (e.g. controller, style, etc.) with // its parent aura definition type, we must find the parent's file properties // and pass those along to processMdapiFileProperty() const bundleDefinitionProperty = []; if (metadataType instanceof auraDefinitionBundleMetadataType_1.AuraDefinitionBundleMetadataType) { const definitionFileProperty = auraDefinitionBundleMetadataType_1.AuraDefinitionBundleMetadataType.getCorrespondingAuraDefinitionFileProperty(retrieveRoot, fileProperties.fileName, metadataType.getMetadataName(), sourceWorkspaceAdapter.metadataRegistry); bundleDefinitionProperty.push(definitionFileProperty); } if (metadataType instanceof lightningComponentBundleMetadataType_1.LightningComponentBundleMetadataType) { const metadataRegistry = sourceWorkspaceAdapter.metadataRegistry; const typeDefObj = metadataRegistry.getTypeDefinitionByMetadataName(metadataType.getMetadataName()); const bundle = new lightningComponentBundleMetadataType_1.LightningComponentBundleMetadataType(typeDefObj); const definitionFileProperty = bundle.getCorrespondingLWCDefinitionFileProperty(retrieveRoot, fileProperties.fileName, metadataType.getMetadataName(), sourceWorkspaceAdapter.metadataRegistry); bundleDefinitionProperty.push(definitionFileProperty); } if (metadataType instanceof waveTemplateBundleMetadataType_1.WaveTemplateBundleMetadataType) { const definitionFileProperty = { type: metadataType.getMetadataName(), fileName: fileProperties.fileName, fullName, }; bundleDefinitionProperty.push(definitionFileProperty); } const element = sourceWorkspaceAdapter.processMdapiFileProperty(sourceElements, retrieveRoot, fileProperties, bundleDefinitionProperty); if (!element) { this.logger.warn(`Unsupported type: ${metadataType.getMetadataName()} path: ${pathWithPackage}`); } }; /** * Process one file path within a metadata package directory * * @param {object} item - the path item * @param {object} metadataRegistry - describe metadata * @param {object} sourceWorkspaceAdapter - workspace adapter * @param {AggregateSourceElements} sourceElements - accumulator of created AggregateSourceElements * @private */ const _processPath = function (item, metadataRegistry, sourceWorkspaceAdapter, sourceElements) { const pkgRelativePath = path.relative(this._package_root, item.path); if (pkgRelativePath.length > 0) { // Ignore the package root itself const metadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMdapiPackagePath(pkgRelativePath, metadataRegistry); if (metadataType) { if (!item.path.endsWith(MetadataRegistry.getMetadataFileExt()) || metadataType.isFolderType()) { const pathWithPackage = path.join(path.basename(this._package_root), pkgRelativePath); const fullName = metadataType.getAggregateFullNameFromMdapiPackagePath(pkgRelativePath); if (item.stats.isFile()) { _processFile.call(this, metadataType, pathWithPackage, fullName, sourceWorkspaceAdapter, sourceElements); } } else { if (metadataType.hasContent()) { const indexOfMetaExt = item.path.indexOf(MetadataRegistry.getMetadataFileExt()); const retrievedContentPath = item.path.substring(0, indexOfMetaExt); const throwMissingContentError = () => { const err = new Error(); err['name'] = 'Missing content file'; err['message'] = messages.getMessage('MissingContentFile', retrievedContentPath); throw err; }; // LightningComponentBundleMetadataTypes can have a .css or .js file as the main content // so check for both before erroring. if (metadataType instanceof lightningComponentBundleMetadataType_1.LightningComponentBundleMetadataType) { const cssContentPath = retrievedContentPath.replace(/\.js$/, '.css'); if (!core_1.fs.existsSync(retrievedContentPath) && !core_1.fs.existsSync(cssContentPath)) { throwMissingContentError(); } } else { // Skipping content file validation for ExperienceBundle metadata type since it is // a special case and does not have a corresponding content file. if (metadataType.getMetadataName() !== 'ExperienceBundle' && !core_1.fs.existsSync(retrievedContentPath)) { throwMissingContentError(); } } } } } else { this.logger.warn(`The type definition cannot be found for ${item.path}`); } } }; /** * Converts an array of aggregateSourceElements into objects suitable for a return to the caller. * * @returns {[{state, fullName, type, filePath}]} */ const _mapToOutputElements = function (aggregateSourceElements) { let allWorkspaceElements = []; aggregateSourceElements.getAllSourceElements().forEach((aggregateSourceElement) => { allWorkspaceElements = allWorkspaceElements.concat(aggregateSourceElement.getWorkspaceElements()); }); return allWorkspaceElements.map((workspaceElement) => { const fullFilePath = workspaceElement.getSourcePath(); const paths = fullFilePath.split(this.projectPath); let filePath = paths[paths.length - 1]; // Remove the leading slash if (filePath && path.isAbsolute(filePath)) { filePath = filePath.substring(1); } return { fullName: workspaceElement.getFullName(), type: workspaceElement.getMetadataName(), filePath, state: workspaceFileState_1.toReadableState(workspaceElement.getState()), }; }); }; /** * Finds the filepath root containing the package.xml * * @private */ const _setPackageRoot = function () { const packageDotXmlPath = `${this.root}${path.sep}package.xml`; return glob(packageDotXmlPath).then((outerfiles) => { if (outerfiles.length > 0) { this._package_root = this.root; return BBPromise.resolve(); } else { const packageDotXmlGlobPath = `${this.root}${path.sep}**${path.sep}package.xml`; if (this.logger.isDebugEnabled()) { this.logger.debug(`Looking for package.xml here ${packageDotXmlGlobPath}`); } return glob(packageDotXmlGlobPath).then((innerfiles) => { if (innerfiles.length < 1) { const error = new Error(); error['code'] = 'ENOENT'; throw error; } this._package_root = path.dirname(innerfiles[0]); return BBPromise.resolve(); }); } }); }; /** * An api class for converting a source directory in mdapi package format into source compatible with an SFDX workspace. */ class MdapiConvertApi { constructor(force) { this.force = optional.ofNullable(force).orElse(new Force()); this.projectPath = this.force.getConfig().getProjectPath(); this._outputDirectory = this.force.getConfig().getAppConfig().defaultPackagePath; this.logger = logger.child('mdapiConvertApi'); } /** * @returns {string} the directory for the output */ get outputDirectory() { return this._outputDirectory; } /** * set the value of the output directory * * @param {string} outputDirectory - the new value of the output directory. */ set outputDirectory(outputDirectory) { if (_.isString(outputDirectory) && !_.isEmpty(outputDirectory)) { if (path.isAbsolute(outputDirectory)) { this._outputDirectory = path.relative(process.cwd(), outputDirectory); } else { this._outputDirectory = outputDirectory; } } else { throw almError('InvalidParameter', ['outputdir', outputDirectory]); } } /** * @returns {string} value of the root directory to convert. default to the project directory */ get root() { return this._root; } /** * set the value of the root directory to convert * * @param {string} sourceRootValue - a directory containing a package.xml file. Is should represents a valid mdapi * package. */ set root(sourceRootValue) { if (sourceRootValue && typeof sourceRootValue === 'string' && sourceRootValue.trim().length > 0) { this._root = _normalizePath(sourceRootValue); } else { throw almError('InvalidParameter', ['sourceRootValue', sourceRootValue]); } } isValidSourcePath(sourcePath) { const isValid = this.forceIgnore.accepts(sourcePath); // Skip directories/files beginning with '.' and that should be ignored return isValid && !path.basename(sourcePath).startsWith('.'); } /** * @param itemPath path of the metadata to convert * @param mdName name of metadata as given in -m parameter * @returns true if the file is a folder metadata type */ isFolder(itemPath, mdName) { return mdName && itemPath.endsWith(`${path.sep}${mdName.split(path.sep)[0]}-meta.xml`); } /** * @param itemPath the path to the file in the local project * @param validMetatdata a filter against which the paths would be checked to see if the file needs to be converted * @param metadataRegistry {MetadataRegistry} * @returns { boolean} returns true if the path is valid path for covert */ checkMetadataFromType(itemPath, validMetatdata, metadataRegistry) { const typDef = metadataRegistry.getTypeDefinitionByFileName(itemPath); for (const md of validMetatdata) { const [mdType, mdName] = md.split(':'); if (!mdName && typDef) { if (mdType === typDef.metadataName) { return true; } } else if (mdName) { if (itemPath.includes(mdName) || this.isFolder(itemPath, mdName)) { return true; } } } return false; } /** * @param itemPath the path to the file in the local project * @param validMetatdata a filter against which the paths would be checked to see if the file needs to be converted */ checkMetadataFromPath(itemPath, validMetatdata) { // eslint-disable-next-line @typescript-eslint/no-shadow for (const path of validMetatdata) { if (itemPath.includes(path)) { return true; } } return false; } /** * @param typeNamePairs type name pairs from manifest * @param itemPath the path to the file in the local project * @param metadataRegistry * @returns {boolean} true if the metadata type or, file name is present in the manifest */ checkMetadataFromManifest(typeNamePairs, itemPath, metadataRegistry) { const typDef = metadataRegistry.getTypeDefinitionByFileName(itemPath); const metadataName = sourcePathUtil_1.getFileName(itemPath); let foundInManifest = false; typeNamePairs.forEach((entry) => { if (typDef && entry.name.includes('*') && foundInManifest === false) { if (typDef.metadataName === entry.type) { foundInManifest = true; } } if (metadataName === entry.name) { foundInManifest = true; } else if (itemPath.includes(entry.name)) { /** For folder type structure */ foundInManifest = true; } }); return foundInManifest; } /** * Returns a promise to convert a metadata api directory package into SFDX compatible source. * * @returns {BBPromise} */ convertSource(org, context) { // Walk the metadata package elements. let validMetatdata; return org .resolveDefaultName() .then(() => fsx_ensureDir(this.root)) .then(() => fsx_ensureDir(this._outputDirectory)) .then(() => _setPackageRoot.call(this)) .then(() => { if (context) { if (context.manifest) { return parseManifestEntriesArray_1.parseToManifestEntriesArray(context.manifest); } if (context.metadata) { return context.metadata.split(','); } else if (context.metadatapath) { return context.metadatapath.split(','); } } }) .then((result) => { validMetatdata = result; return sourceWorkspaceAdapter_1.SourceWorkspaceAdapter.create({ org, metadataRegistryImpl: MetadataRegistry, defaultPackagePath: path.relative(this.projectPath, this.outputDirectory), fromConvert: true, }); }) .then((sourceWorkspaceAdapter) => { if (this.logger.isDebugEnabled()) { [ { name: 'root', value: this.root }, { name: 'outputdir', value: this._outputDirectory }, ].forEach((attribute) => { this.logger.debug(`Processing mdapi convert with ${attribute.name}: ${attribute.value}`); }); } this.logger.debug(`Processing mdapi convert with package root: ${this._package_root}`); const metadataRegistry = sourceWorkspaceAdapter.metadataRegistry; const aggregateSourceElements = new aggregateSourceElements_1.AggregateSourceElements(); this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this._package_root); // Use a "new" promise to block the promise chain until the source metadata package is processed. return new BBPromise((resolve, reject) => { let errorFoundProcessingPath = false; klaw(this._package_root) .on('data', (item) => { try { if (this.isValidSourcePath(item.path)) { if (!validMetatdata) { _processPath.call(this, item, metadataRegistry, sourceWorkspaceAdapter, aggregateSourceElements); } else if (context.metadatapath) { const isValidMetatadataPath = this.checkMetadataFromPath(item.path, validMetatdata); if (isValidMetatadataPath) { _processPath.call(this, item, metadataRegistry, sourceWorkspaceAdapter, aggregateSourceElements); } } else if (context.manifest) { const foundInManifest = this.checkMetadataFromManifest(validMetatdata, item.path, metadataRegistry); if (foundInManifest) { _processPath.call(this, item, metadataRegistry, sourceWorkspaceAdapter, aggregateSourceElements); } } else if (context.metadata) { const validMetatdataType = this.checkMetadataFromType(item.path, validMetatdata, metadataRegistry); if (validMetatdataType) { _processPath.call(this, item, metadataRegistry, sourceWorkspaceAdapter, aggregateSourceElements); } } } } catch (e) { this.logger.debug(e.message); errorFoundProcessingPath = true; if (e.name === 'Missing metadata file' || e.name === 'Missing content file') { reject(e); } else { const message = messages.getMessage('errorProcessingPath', [item.path], 'mdapiConvertApi'); const error = new core_1.SfdxError(message, 'errorProcessingPath', undefined, undefined, e); reject(error); } } }) // eslint-disable-next-line @typescript-eslint/no-misused-promises .on('end', async () => { if (!errorFoundProcessingPath && !aggregateSourceElements.isEmpty()) { await sourceWorkspaceAdapter.updateSource(aggregateSourceElements, undefined, true /** checkForDuplicates */, this.unsupportedMimeTypes); if (this.logger.isDebugEnabled()) { const allPaths = []; aggregateSourceElements.getAllSourceElements().forEach((sourceElement) => { const workspaceElements = sourceElement.getWorkspaceElements(); workspaceElements.forEach((workspaceElement) => { allPaths.push(workspaceElement.getSourcePath()); }); }); this.logger.debug(allPaths); } } resolve(_mapToOutputElements.call(this, aggregateSourceElements)); }) .on('error', (err, item) => { reject(almError({ keyName: 'errorProcessingPath', bundle: 'mdapiConvertApi' }, [item.path])); }); }); }) .catch((err) => { // Catch invalid source package. if (err.code && err.code === 'ENOENT') { throw almError({ keyName: 'invalidPath', bundle: 'mdapiConvertApi' }); } else { throw err; } }); } } module.exports = MdapiConvertApi; //# sourceMappingURL=mdapiConvertApi.js.map