powerplatform-review-tool
Version:
Evaluate Power Platform solution zip files based on best practice patterns
368 lines (367 loc) • 17.5 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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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.reviewPowerAppsComponents = reviewPowerAppsComponents;
exports.readPropertiesFile = readPropertiesFile;
exports.processYamlFiles = processYamlFiles;
const jszip_1 = __importDefault(require("jszip"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const logger_1 = __importStar(require("../utilities/logger"));
const ManifestConstant_1 = require("../ManifestConstant");
const powerappsPatterns_1 = require("./powerappsPatterns");
const appHelper_1 = require("../utilities/appHelper");
async function reviewPowerAppsComponents(zipContent, patternDetails) {
// Cache the folder lookup
const canvasAppsFolder = zipContent.folder("CanvasApps");
const canvasAppsList = [];
let totalApps = 0;
const powerAppsPatternCount = Object.keys(ManifestConstant_1.PowerAppsPatternInfo).length;
if (!canvasAppsFolder) {
logger_1.default.error("No CanvasApps folder found in the solution.");
return {
items: canvasAppsList,
totalApps: 0,
totalScore: "0%",
};
}
const processingPromises = [];
canvasAppsFolder.forEach((relativePath, file) => {
if (relativePath.endsWith(".msapp" /* StringConstants.MSAPP_EXTENSION */)) {
totalApps++;
processingPromises.push(processAppFile(file, canvasAppsList, patternDetails));
}
});
await Promise.all(processingPromises);
const totalScore = canvasAppsList.reduce((acc, app) => acc + (Number(app.score.replace("%", "")) || 0), 0);
return {
items: canvasAppsList,
totalApps: canvasAppsList.length,
totalScore: canvasAppsList.length ? Math.round(totalScore / canvasAppsList.length).toString() + "%" : "0%",
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
async function processAppFile(file, canvasAppsList, patternDetails) {
try {
const appZipContent = await file.async("arraybuffer" /* StringConstants.ARRAYBUFFER */);
const appZip = await jszip_1.default.loadAsync(appZipContent);
const msapp = (0, appHelper_1.createMsApp)();
// Read properties.json early to check DocumentType
const propertiesFile = appZip.file("Properties.json");
if (propertiesFile) {
try {
const propertiesContent = await propertiesFile.async("string" /* StringConstants.STRING */);
const propertiesJson = JSON.parse(propertiesContent);
msapp.appSettings = propertiesJson;
// **Skip processing if the app is a Component Library**
if (propertiesJson.DocumentType === "ComponentLibrary") {
console.log(`Skipping Component Library: ${propertiesJson.Name}`);
return; // Exit function early
}
}
catch (error) {
(0, logger_1.logError)(error, "Error parsing Properties.json");
}
}
else {
logger_1.default.error("Properties.json file not found in the .msapp file.");
}
// Continue processing the app if it's not a Component Library
const resourcesFolder = appZip.folder("Resources\\");
await Promise.all([
countCodeComponents(resourcesFolder, msapp),
countCanvasComponents(appZip, msapp),
readDataSourcesFile(appZip, msapp),
readAppCheckerResult(appZip, msapp),
processMediaFiles(appZip, msapp),
processYamlFiles(appZip, msapp),
]);
const currentAppResults = [];
const processedPatternIDs = new Set(); // Initialize the Set to track processed pattern IDs
let score = 0;
const patterns = [
powerappsPatterns_1.patternCheckMediaFileSize,
powerappsPatterns_1.patternCheckUnusedMediaResources,
powerappsPatterns_1.patternCheckAppSettings,
powerappsPatterns_1.patternCheckAppCheckerAccessibilityIssues,
powerappsPatterns_1.patternCheckUnusedElements,
powerappsPatterns_1.patternCheckDelayLoading,
];
for (const pattern of patterns) {
try {
const patternResult = await pattern(msapp);
// Check if the patternID has already been processed
if (patternDetails && patternDetails.length > 0 && !processedPatternIDs.has(patternResult.patternID)) {
const matchingPatternDetail = patternDetails.find((detail) => detail.data.patternID === patternResult.patternID);
if (matchingPatternDetail) {
const data = matchingPatternDetail.data;
patternResult.patternName =
data.patternName != null && data.patternName !== "" ? data.patternName : patternResult.patternName;
patternResult.description =
data.description != null && data.description !== "" ? data.description : patternResult.description;
patternResult.docLinks =
data.docLinks != null && data.docLinks !== "" ? data.docLinks : patternResult.docLinks;
patternResult.recommendation =
data.recommendation != null && data.recommendation !== ""
? data.recommendation
: patternResult.recommendation;
patternResult.severity =
data.severity != null && data.severity !== "" ? data.severity : patternResult.severity;
patternResult.category =
data.category != null && data.category !== "" ? data.category : patternResult.category;
}
}
currentAppResults.push(patternResult);
processedPatternIDs.add(patternResult.patternID); // Add the patternID to the Set
if (patternResult.status === "Pass" /* ManifestPropertyStatusNames.Pass */) {
score++;
}
if (patternResult.status === "Warning" /* ManifestPropertyStatusNames.Warning */) {
score += 0.5;
}
}
catch (error) {
if (error instanceof Error) {
logger_1.default.error(`Error processing pattern: ${error.message}`);
}
else {
logger_1.default.error("Unknown error processing pattern");
}
}
}
const failures = (0, appHelper_1.iterateThroughScreens)(msapp);
// FOR YAML-SKIP: Determine whether to skip YAML-based patterns
const skipYamlBasedPatterns = !msapp.yaml ||
Object.keys(msapp.yaml).length === 0 ||
(Object.keys(msapp.yaml).length === 1 && !msapp.yaml.Screens && !msapp.yaml.App);
const failurePatternResults = (0, appHelper_1.processFailures)(failures, patternDetails, skipYamlBasedPatterns);
// Update score based on failure pattern results
for (const patternResult of failurePatternResults) {
// Check if the patternID has already been processed
if (!processedPatternIDs.has(patternResult.patternID)) {
if (patternResult.status === "Pass" /* ManifestPropertyStatusNames.Pass */) {
score++;
}
if (patternResult.status === "Warning" /* ManifestPropertyStatusNames.Warning */) {
score += 0.5;
}
currentAppResults.push(patternResult);
processedPatternIDs.add(patternResult.patternID); // Add the patternID to the Set
}
}
canvasAppsList.push({
name: msapp.appSettings.Name.toLowerCase().endsWith(".msapp")
? decodeURI(msapp.appSettings.Name.substring(0, msapp.appSettings.Name.length - 6))
.replace(/%2B/g, " ")
.replace(/%2b/g, "+") ||
msapp.appSettings.Name ||
"Unknown App Name"
: msapp.appSettings.Name,
result: currentAppResults,
totalScreens: msapp.totalScreens || 0,
totalCanvasComponents: msapp.totalCanvasComponents || 0,
totalCodeComponents: msapp.totalCodeComponents || 0,
score: score > 0 ? Math.round((score / Object.keys(ManifestConstant_1.PowerAppsPatternInfo).length) * 100) + "%" : "0%",
});
}
catch (error) {
(0, logger_1.logError)(error, "Error processing pattern app file");
}
}
async function readPropertiesFile(appZip, msapp) {
const propertiesFile = appZip.file("Properties.json");
if (propertiesFile) {
try {
const propertiesContent = await propertiesFile.async("string" /* StringConstants.STRING */);
const propertiesJson = JSON.parse(propertiesContent);
msapp.appSettings = propertiesJson;
}
catch (error) {
(0, logger_1.logError)(error, "Error parsing Properties.json");
}
}
else {
logger_1.default.error("Properties.json file not found in the .msapp file.");
}
}
async function countCodeComponents(resourcesFolder, msapp) {
if (resourcesFolder) {
const controlsFolder = resourcesFolder.folder("Controls");
if (controlsFolder) {
const controlFiles = Object.keys(controlsFolder.files).filter((fileName) => fileName.endsWith("bundle.js"));
msapp.totalCodeComponents = controlFiles.length;
}
}
}
async function countCanvasComponents(appZip, msapp) {
let componentsCount = 0;
const componentsFolder = appZip.folder("Components\\");
if (componentsFolder) {
const componentFiles = Object.keys(componentsFolder.files).filter((key) => !componentsFolder.files[key].dir && key.startsWith("Components\\") && key.endsWith(".json"));
componentsCount = componentFiles.length;
}
else {
logger_1.default.error("Components folder not found in the .msapp file.");
}
msapp.totalCanvasComponents = componentsCount;
logger_1.default.info(`Total canvas component files detected: ${componentsCount}`);
}
async function readDataSourcesFile(appZip, msapp) {
const dataSourcesFile = appZip.file("References\\DataSources.json");
if (dataSourcesFile) {
try {
const dataSourcesContent = await dataSourcesFile.async("string");
const dataSourcesJson = JSON.parse(dataSourcesContent);
msapp.dataSources = dataSourcesJson.DataSources;
}
catch (error) {
(0, logger_1.logError)(error, "Error parsing DataSources.json");
}
}
else {
logger_1.default.error("DataSources.json file not found in the References folder.");
}
}
async function readAppCheckerResult(appZip, msapp) {
const sarifFile = appZip.file("AppCheckerResult.sarif");
if (sarifFile) {
try {
const sarifContent = await sarifFile.async("string");
const sarifJson = JSON.parse(sarifContent);
msapp.appCheckerIssues = extractAppCheckerIssues(sarifJson);
}
catch (error) {
(0, logger_1.logError)(error, "Error parsing AppCheckerResult.sarif");
}
}
else {
logger_1.default.error("AppCheckerResult.sarif file not found in the .msapp file.");
}
}
function extractAppCheckerIssues(sarifJson) {
if (!sarifJson.runs)
return [];
return sarifJson.runs.flatMap((run) => {
const rulesMap = new Map(run.tool?.driver?.rules?.map((rule) => [rule.id, rule]));
return (run.results?.map((result) => {
const rule = rulesMap.get(result.ruleId);
return {
RuleId: result.ruleId,
HowToFix: rule?.properties?.howToFix || "No recommendation provided",
WhyFix: rule?.properties?.whyFix || "",
Message: rule?.messageStrings?.issue?.text || "No message provided",
Location: result.locations?.[0]?.physicalLocation?.address?.fullyQualifiedName || "Unknown Location",
Severity: result.properties?.level || "Unknown Severity",
Category: rule?.properties?.primaryCategory || "",
};
}) || []);
});
}
async function processMediaFiles(appZip, msapp) {
const imagesFolder = appZip.folder("Assets\\Images");
if (imagesFolder) {
const imageFiles = Object.keys(imagesFolder.files).filter((key) => !imagesFolder.files[key].dir && key.startsWith("Assets\\Images\\" /* StringConstants.ASSETS_IMAGES_FOLDER */));
const mediaFilesProcessing = [];
for (const fileName of imageFiles) {
const file = appZip.file(fileName);
if (file) {
const processingPromise = file
.async("blob")
.then((fileContent) => {
const mediaFileSize = fileContent.size;
const mediaFileName = fileName.split("/").pop() || fileName;
msapp.assets.push({
mediaName: mediaFileName,
mediaSize: mediaFileSize,
exceedsThreshold: mediaFileSize > ManifestConstant_1.MEDIA_FILE_SIZE_THRESHOLD,
});
})
.catch((error) => {
logger_1.default.error(`Failed to process file ${fileName}: ${error.message}`);
});
mediaFilesProcessing.push(processingPromise);
}
}
await Promise.all(mediaFilesProcessing);
}
}
// eslint-disable-next-line sonarjs/cognitive-complexity
async function processYamlFiles(appZip, msapp) {
const srcFolder = appZip.folder("Src\\"); // StringConstants.SRC_FOLDER
if (srcFolder) {
const srcFiles = Object.keys(srcFolder.files).filter((key) => !srcFolder.files[key].dir && key.startsWith("Src\\"));
const fileProcessingPromises = [];
let totalScreens = 0;
const result = {};
for (const fileName of srcFiles) {
const file = appZip.file(fileName);
totalScreens++;
if (file) {
const processingPromise = file
.async("string")
.then((yamlContent) => {
if (yamlContent) {
const parsedYaml = js_yaml_1.default.load(yamlContent);
const screenOrComponentName = fileName.split(/[/\\]/).pop()?.replace(".pa.yaml", "");
// Handle App level case
if (screenOrComponentName === "App") {
result.App = parsedYaml.App;
}
// Handle Screen case
else if (screenOrComponentName && parsedYaml.Screens?.[screenOrComponentName]) {
result.Screens = result.Screens || {}; // Initialize if undefined
result.Screens[screenOrComponentName] = parsedYaml.Screens[screenOrComponentName];
}
}
})
.catch((error) => {
logger_1.default.error(`Failed to process file ${fileName}: ${error.message}`);
});
fileProcessingPromises.push(processingPromise);
}
else {
logger_1.default.error(`File not found in JSZip structure for path: ${fileName}`);
}
}
await Promise.all(fileProcessingPromises);
msapp.totalScreens = totalScreens ? totalScreens - 1 : 0;
msapp.yaml = result;
}
else {
logger_1.default.error("Src folder not found in the .msapp file.");
}
}