salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
540 lines (538 loc) • 21.2 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
*/
// Node
const path = require("path");
const util = require("util");
// 3pp
const _ = require("lodash");
const core_1 = require("@salesforce/core");
// Constants
const METADATA_FILE_EXT = '-meta.xml';
const LWC_FOLDER_NAME = 'lwc';
const _lightningDefTypes = {
APPLICATION: {
defType: 'APPLICATION',
format: 'XML',
fileSuffix: '.app',
hasMetadata: true,
},
CONTROLLER: {
defType: 'CONTROLLER',
format: 'JS',
fileSuffix: 'Controller.js',
},
COMPONENT: {
defType: 'COMPONENT',
format: 'XML',
fileSuffix: '.cmp',
hasMetadata: true,
},
EVENT: {
defType: 'EVENT',
format: 'XML',
fileSuffix: '.evt',
hasMetadata: 'true',
},
HELPER: {
defType: 'HELPER',
format: 'JS',
fileSuffix: 'Helper.js',
},
INTERFACE: {
defType: 'INTERFACE',
format: 'XML',
fileSuffix: '.intf',
hasMetadata: true,
},
RENDERER: {
defType: 'RENDERER',
format: 'JS',
fileSuffix: 'Renderer.js',
},
STYLE: {
defType: 'STYLE',
format: 'CSS',
fileSuffix: '.css',
},
PROVIDER: {
defType: 'PROVIDER',
format: 'JS',
fileSuffix: 'Provider.js',
},
MODEL: {
defType: 'MODEL',
format: 'JS',
fileSuffix: 'Model.js',
},
TESTSUITE: {
defType: 'TESTSUITE',
format: 'JS',
fileSuffix: 'Test.js',
},
DOCUMENTATION: {
defType: 'DOCUMENTATION',
format: 'XML',
fileSuffix: '.auradoc',
},
TOKENS: {
defType: 'TOKENS',
format: 'XML',
fileSuffix: '.tokens',
hasMetadata: true,
},
DESIGN: {
defType: 'DESIGN',
format: 'XML',
fileSuffix: '.design',
},
SVG: {
defType: 'SVG',
format: 'SVG',
fileSuffix: '.svg',
},
};
const _lwcDefTypes = {
MODULE_RESOURCE_JS: {
defType: 'MODULE',
format: 'JS',
fileSuffix: '.js',
},
MODULE_RESOURCE_HTML: {
defType: 'MODULE',
format: 'HTML',
fileSuffix: '.html',
},
MODULE_RESOURCE_CSS: {
defType: 'MODULE',
format: 'CSS',
fileSuffix: '.css',
},
MODULE_RESOURCE_SVG: {
defType: 'MODULE',
format: 'SVG',
fileSuffix: '.svg',
},
MODULE_RESOURCE_XML: {
defType: 'MODULE',
format: 'XML',
fileSuffix: '.xml',
},
};
const _waveDefTypes = {
JSON: {
defType: 'JSON',
format: 'JSON',
fileSuffix: '.json',
},
HTML: {
defType: 'HTML',
format: 'HTML',
fileSuffix: '.html',
},
CSV: {
defType: 'CSV',
format: 'CSV',
fileSuffix: '.csv',
},
XML: {
defType: 'XML',
format: 'XML',
fileSuffix: '.xml',
},
TXT: {
defType: 'TXT',
format: 'TXT',
fileSuffix: '.txt',
},
IMG: {
defType: 'IMG',
format: 'IMG',
fileSuffix: '.img',
},
JPG: {
defType: 'JPG',
format: 'JPG',
fileSuffix: '.jpg',
},
JPEG: {
defType: 'JPEG',
format: 'JPEG',
fileSuffix: '.jpeg',
},
GIF: {
defType: 'GIF',
format: 'GIF',
fileSuffix: '.gif',
},
PNG: {
defType: 'PNG',
format: 'PNG',
fileSuffix: '.png',
},
};
const _typeDefMatchesDecompositionExtension = function (typeDef, typeExtension) {
if (!util.isNullOrUndefined(typeDef.decompositionConfig) &&
!util.isNullOrUndefined(typeDef.decompositionConfig.decompositions)) {
for (const decomposition of typeDef.decompositionConfig.decompositions) {
if (decomposition.ext.toLowerCase() === typeExtension.toLowerCase()) {
return true;
}
}
}
return false;
};
const _typeDefMatchesExtension = function (typeDef, typeExtension, includeDecomposedSubtypes) {
if (!util.isNullOrUndefined(typeDef.ext) && typeDef.ext.toLowerCase() === typeExtension) {
return true;
}
else if (includeDecomposedSubtypes) {
return _typeDefMatchesDecompositionExtension(typeDef, typeExtension);
}
else {
return false;
}
};
const _getDecompositionByName = function (typeDefs, value) {
if (util.isNullOrUndefined(value)) {
return null;
}
let foundDecomposition;
Object.keys(typeDefs).forEach((key) => {
if (!util.isNullOrUndefined(typeDefs[key].decompositionConfig)) {
typeDefs[key].decompositionConfig.decompositions.forEach((decomposition) => {
if (decomposition.metadataName === value) {
foundDecomposition = decomposition;
}
});
}
});
return util.isNullOrUndefined(foundDecomposition) ? null : foundDecomposition;
};
class MetadataRegistry {
constructor() {
this.typeDefs = this.getMetadataTypeDefs();
this.typeDirectories = this.getTypeDirectories();
this.lightningDefTypes = _lightningDefTypes;
this.waveDefTypes = _waveDefTypes;
this.lwcDefTypes = _lwcDefTypes;
this.typeDefsByExtension = this.getTypeDefsByExtension();
this.metadataFileExt = METADATA_FILE_EXT;
this.projectPath = core_1.SfdxProject.resolveProjectPathSync();
}
isSupported(metadataName) {
const isSupportedType = !util.isNullOrUndefined(this.getTypeDefinitionByMetadataName(metadataName));
if (isSupportedType) {
return true;
}
const decomposedSubtype = _getDecompositionByName(this.typeDefs, metadataName);
return !util.isNullOrUndefined(decomposedSubtype) && decomposedSubtype.isAddressable;
}
static getMetadataFileExt() {
return METADATA_FILE_EXT;
}
getMetadataTypeDefs() {
if (!this.typeDefs) {
const metadataInfos = require(path.join(__dirname, '..', '..', '..', 'metadata', 'metadataTypeInfos.json'));
return metadataInfos.typeDefs;
}
else {
return this.typeDefs;
}
}
/**
* Returns a formatted key provided a metadata type and name. This is used as a unique
* identifier for metadata. E.g., `ApexClass__MyClass`
*
* @param metadataType The metadata type. E.g., `ApexClass`
* @param metadataName The metadata name. E.g., `MyClass`
*/
static getMetadataKey(metadataType, metadataName) {
return `${metadataType}__${metadataName}`;
}
// Returns list of default directories for all metadata types
getTypeDirectories() {
if (util.isNullOrUndefined(this.typeDirectories)) {
const metadataTypeInfos = this.getMetadataTypeDefs();
return Object.values(metadataTypeInfos).map((i) => i.defaultDirectory);
}
else {
return this.typeDirectories;
}
}
getTypeDefsByExtension() {
const typeDefsByExtension = new Map();
Object.keys(this.typeDefs).forEach((metadataName) => {
const metadataTypeExtension = this.typeDefs[metadataName].ext;
typeDefsByExtension.set(metadataTypeExtension, this.typeDefs[metadataName]);
});
return typeDefsByExtension;
}
getLightningDefByFileName(fileName) {
return this.lightningDefTypes[Object.keys(this.lightningDefTypes).find((key) => {
const lightningDefType = this.lightningDefTypes[key];
return fileName.endsWith(lightningDefType.fileSuffix);
})];
}
getWaveDefByFileName(fileName) {
return this.waveDefTypes[Object.keys(this.waveDefTypes).find((key) => {
const waveDefType = this.waveDefTypes[key];
return fileName.endsWith(waveDefType.fileSuffix);
})];
}
getLightningDefByType(type) {
return this.lightningDefTypes[Object.keys(this.lightningDefTypes).find((key) => {
const lightningDefType = this.lightningDefTypes[key];
return type === lightningDefType.defType;
})];
}
/**
* Returns the array of typeDefs where the default directory of each typeDef matches the passed in 'name' param
*
* @param name
* @returns {any[]}
*/
getTypeDefinitionsByDirectoryName(name) {
const metadataNames = Object.keys(this.typeDefs).filter((key) => this.typeDefs[key].defaultDirectory === name);
return metadataNames.map((metadataName) => this.typeDefs[metadataName]);
}
getTypeDefinitionByMetadataName(metadataName) {
let typeDef = this.typeDefs[metadataName];
if (!typeDef && metadataName.endsWith('Settings')) {
// even though there is one "Settings" in the describeMetadata response when you retrieve a setting it comes
// down as "AccountSettings", "CaseSettings", etc. so here we account for that scenario.
typeDef = this.typeDefs['Settings'];
}
if (!typeDef && metadataName.endsWith('CustomLabel')) {
typeDef = this.typeDefs['CustomLabels'];
}
return typeDef;
}
// given file extension, return type def
getTypeDefinitionByFileName(filePath, useTrueExtType) {
if (util.isNullOrUndefined(filePath)) {
return null;
}
let workspaceFilePath = filePath;
if (filePath.startsWith(this.projectPath)) {
workspaceFilePath = filePath.substring(core_1.SfdxProject.resolveProjectPathSync().length, filePath.length);
}
if (workspaceFilePath.includes(`${path.sep}aura${path.sep}`)) {
return this.typeDefs.AuraDefinitionBundle;
}
if (workspaceFilePath.includes(`${path.sep}waveTemplates${path.sep}`)) {
return this.typeDefs.WaveTemplateBundle;
}
if (workspaceFilePath.includes(`${path.sep}${this.typeDefs.ExperienceBundle.defaultDirectory}${path.sep}`)) {
return this.typeDefs.ExperienceBundle;
}
if (workspaceFilePath.includes(`${path.sep}${LWC_FOLDER_NAME}${path.sep}`)) {
return this.typeDefs.LightningComponentBundle;
}
if (workspaceFilePath.includes(`${path.sep}${this.typeDefs.CustomSite.defaultDirectory}${path.sep}`)) {
return this.typeDefs.CustomSite;
}
// CustomObject file names are special, they are all named "object-meta.xml"
if (path.basename(workspaceFilePath) === this.typeDefs.CustomObject.ext + this.metadataFileExt) {
return this.typeDefs.CustomObject;
}
const typeDefWithNonStandardExtension = this.getTypeDefinitionByFileNameWithNonStandardExtension(workspaceFilePath);
if (!_.isNil(typeDefWithNonStandardExtension)) {
return typeDefWithNonStandardExtension;
}
if (workspaceFilePath.endsWith(this.metadataFileExt)) {
workspaceFilePath = workspaceFilePath.substring(0, workspaceFilePath.indexOf(this.metadataFileExt));
}
let typeExtension = path.extname(workspaceFilePath);
if (util.isNullOrUndefined(typeExtension)) {
return null;
}
typeExtension = typeExtension.replace('.', '');
const defs = Object.values(this.typeDefs);
const defaultDirectory = path
.dirname(workspaceFilePath)
.split(path.sep)
.find((i) => !!i && this.typeDirectories.includes(i));
let typeDef;
if (defaultDirectory) {
typeDef = defs.find((def) => def.ext === typeExtension && def.defaultDirectory === defaultDirectory);
}
if (_.isNil(typeDef)) {
typeDef = this.typeDefsByExtension.get(typeExtension);
}
if (!_.isNil(typeDef)) {
if (!_.isNil(useTrueExtType) && useTrueExtType) {
return typeDef;
}
if (!_.isNil(typeDef.parent)) {
return typeDef.parent;
}
return typeDef;
}
return null;
}
// A document must be co-resident with its metadata file.
// A file from an exploded zip static resource must be within a directory that is co-resident with its metadata file.
getTypeDefinitionByFileNameWithNonStandardExtension(fileName, isDirectoryPathElement, typeDefsToCheck) {
const supportedTypeDefs = [this.typeDefs.Document, this.typeDefs.StaticResource];
const candidateTypeDefs = util.isNullOrUndefined(typeDefsToCheck) ? supportedTypeDefs : typeDefsToCheck;
let typeDef = this.getTypeDefinitionByFileNameWithCoresidentMetadataFile(fileName, candidateTypeDefs, false);
if (util.isNullOrUndefined(typeDef) && candidateTypeDefs.includes(this.typeDefs.StaticResource)) {
typeDef = this.getTypeDefinitionByFileNameWithCoresidentMetadataFile(path.dirname(fileName), [this.typeDefs.StaticResource], true);
}
if (util.isNullOrUndefined(typeDef)) {
typeDef = this.getTypeDefinitionByFileNameMatchingDefaultDirectory(fileName, isDirectoryPathElement, candidateTypeDefs);
}
return typeDef;
}
getTypeDefinitionByFileNameWithCoresidentMetadataFile(fileName, typeDefsToCheck, recurse) {
const dir = path.dirname(fileName);
if (this.isDirPathExpended(dir)) {
return null;
}
const fullName = path.basename(fileName, path.extname(fileName));
// eslint-disable-next-line @typescript-eslint/no-shadow
const typeDef = typeDefsToCheck.find((typeDef) => core_1.fs.existsSync(path.join(dir, `${fullName}.${typeDef.ext}${this.metadataFileExt}`)));
if (!util.isNullOrUndefined(typeDef)) {
return typeDef;
}
return recurse ? this.getTypeDefinitionByFileNameWithCoresidentMetadataFile(dir, typeDefsToCheck, true) : null;
}
getTypeDefinitionByFileNameMatchingDefaultDirectory(fileName, isDirectoryPathElement, typeDefsToCheck) {
const dir = path.dirname(fileName);
if (this.isDirPathExpended(dir)) {
return null;
}
if (typeDefsToCheck.includes(this.typeDefs.Document) && !isDirectoryPathElement) {
const pathElements = fileName.split(path.sep);
if (pathElements.length >= 3 &&
pathElements[pathElements.length - 3] === this.typeDefs.Document.defaultDirectory) {
return this.typeDefs.Document;
}
}
if (typeDefsToCheck.includes(this.typeDefs.StaticResource)) {
if (isDirectoryPathElement) {
if (path.basename(fileName) === this.typeDefs.StaticResource.defaultDirectory) {
return this.typeDefs.StaticResource;
}
}
return this.getTypeDefinitionByFileNameMatchingDefaultDirectory(dir, true, [this.typeDefs.StaticResource]);
}
return null;
}
isDirPathExpended(dir) {
return util.isNullOrUndefined(dir) || dir === path.parse(dir).root || dir === '.';
}
isValidAuraSuffix(suffix) {
const auraTypeDefKey = Object.keys(this.lightningDefTypes).find((key) => {
const fileSuffix = this.lightningDefTypes[key].fileSuffix;
return fileSuffix && fileSuffix === suffix;
});
return !util.isNullOrUndefined(auraTypeDefKey);
}
isValidWaveTemplateSuffix(suffix) {
const wtTypeDefKey = Object.keys(this.waveDefTypes).find((key) => {
const fileSuffix = this.waveDefTypes[key].fileSuffix;
return fileSuffix && fileSuffix === suffix;
});
return !util.isNullOrUndefined(wtTypeDefKey);
}
isValidLwcSuffix(suffix) {
const lwcTypeDefKey = Object.keys(this.lwcDefTypes).find((key) => {
const fileSuffix = this.lwcDefTypes[key].fileSuffix;
return fileSuffix && fileSuffix === suffix;
});
return !util.isNullOrUndefined(lwcTypeDefKey);
}
isValidMetadataExtension(ext) {
const extWithoutPeriod = ext.replace('.', '');
const isValidMetadataExtension = !util.isNullOrUndefined(this.typeDefsByExtension.get(extWithoutPeriod));
return isValidMetadataExtension || this.isValidAuraSuffix(ext) || this.isValidLwcSuffix(ext);
}
isValidDecompositionExtension(ext) {
const extWithoutPeriod = ext.replace('.', '');
const includeDecomposedSubtypes = true;
const typeDefKey = Object.keys(this.typeDefs).find((key) => _typeDefMatchesExtension(this.typeDefs[key], extWithoutPeriod, includeDecomposedSubtypes));
const typeDef = this.typeDefs[typeDefKey];
return !util.isNullOrUndefined(typeDefKey) && typeDef.ext.toLowerCase() !== extWithoutPeriod.toLowerCase();
}
isValidExperienceBundleFile(sourcePath) {
const relativeFilePath = MetadataRegistry.splitOnDirName(`${this.typeDefs.ExperienceBundle.defaultDirectory}${path.sep}`, sourcePath)[1];
const relativePathArray = relativeFilePath.split(path.sep);
if (relativePathArray.length == 1) {
// it should be a meta file
const META_FILE_SUFFIX = '.site';
return relativePathArray[0].endsWith(`${META_FILE_SUFFIX}${this.metadataFileExt}`);
}
// There should be 2 folders /siteName/type and the file name should have a json suffix
return relativePathArray.length == 3 && path.extname(relativePathArray[2]).replace('.', '') === 'json';
}
/**
* @param dirName - metadataObjDirName
* @param pathToSplit - /baseDir/metadataObjDirName(ie, dirName)/bundleFiles
*
* This function is like pathToSplit.split(dirName). except that it splits on the last occurance of dirName
* If there is a parent dir with the same name as metadata object dir name, then pathToSplit.split(dirName) may
* not give desired result, so getting the last occurance of the dir name to split on and splitting based on that
*
* @param pathToSplit - An array with 2 elements in it. pathToSplit[0] - baseDir and pathToSplit[1] - bundleFiles
*/
static splitOnDirName(dirName, pathToSplit) {
const dirStartIndex = pathToSplit.lastIndexOf(dirName);
const dirEndIndex = dirStartIndex + dirName.length;
return [pathToSplit.substring(0, dirStartIndex - 1), pathToSplit.substring(dirEndIndex)];
}
isValidSourceFilePath(sourcePath) {
let fileName = path.basename(sourcePath);
if (fileName.endsWith(this.metadataFileExt)) {
fileName = fileName.substring(0, fileName.indexOf(this.metadataFileExt));
}
const projectPath = core_1.SfdxProject.resolveProjectPathSync();
let workspaceSourcePath = sourcePath;
// Aura / LWC are special
if (sourcePath.startsWith(projectPath)) {
workspaceSourcePath = sourcePath.substring(core_1.SfdxProject.resolveProjectPathSync().length, sourcePath.length);
}
if (workspaceSourcePath.includes(`${path.sep}aura${path.sep}`)) {
const cmpName = path.basename(path.dirname(sourcePath));
const suffix = fileName.substring(cmpName.length, fileName.length);
return this.isValidAuraSuffix(suffix);
}
else if (workspaceSourcePath.includes(`${path.sep}${LWC_FOLDER_NAME}${path.sep}`)) {
const suffix = '.' + fileName.split('.').pop();
return this.isValidLwcSuffix(suffix);
}
else if (workspaceSourcePath.includes(`${path.sep}waveTemplates${path.sep}`)) {
const suffix = '.' + fileName.split('.').pop();
return this.isValidWaveTemplateSuffix(suffix);
}
else if (workspaceSourcePath.includes(`${path.sep}${this.typeDefs.ExperienceBundle.defaultDirectory}${path.sep}`)) {
return this.isValidExperienceBundleFile(workspaceSourcePath);
}
else {
const ext = path.extname(fileName);
if (!util.isNullOrUndefined(ext) && ext.length > 0) {
return (this.isValidMetadataExtension(ext) ||
this.isValidDecompositionExtension(ext) ||
this.getTypeDefinitionByFileNameWithNonStandardExtension(workspaceSourcePath) !== null);
}
else {
return (this.isValidMetadataExtension(fileName) ||
this.getTypeDefinitionByFileNameMatchingDefaultDirectory(workspaceSourcePath, false, [
this.typeDefs.Document,
this.typeDefs.StaticResource,
]) !== null);
}
}
}
isCustomName(name) {
const customNameRegex = new RegExp(/.*__.$/);
return customNameRegex.test(name);
}
}
module.exports = MetadataRegistry;
//# sourceMappingURL=metadataRegistry.js.map