UNPKG

powerplatform-review-tool

Version:

Evaluate Power Platform solution zip files based on best practice patterns

368 lines (367 loc) 17.5 kB
"use strict"; 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."); } }