UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

707 lines (705 loc) • 34.8 kB
import c from "chalk"; import fs from 'fs-extra'; import * as Diff from "diff"; import * as path from "path"; import which from "which"; import { execCommand, git, isDockerRunning, uxLog } from "./index.js"; import { parseFlow } from "./flowVisualiser/flowParser.js"; import { getReportDirectory } from "../../config/index.js"; import moment from "moment"; import { SfError } from "@salesforce/core"; import { PACKAGE_ROOT_DIR } from "../../settings.js"; import { AiProvider } from "../aiProvider/index.js"; import { UtilsAi } from "../aiProvider/utils.js"; import { generatePdfFileFromMarkdown } from "../utils/markdownUtils.js"; import { DocBuilderFlow } from "../docBuilder/docBuilderFlow.js"; import { includeFromFile } from "../docBuilder/docUtils.js"; let IS_MERMAID_AVAILABLE = null; export async function isMermaidAvailable() { if (IS_MERMAID_AVAILABLE !== null) { return IS_MERMAID_AVAILABLE; } const isMmdAvailable = await which("mmdc", { nothrow: true }); IS_MERMAID_AVAILABLE = isMmdAvailable !== null; if (IS_MERMAID_AVAILABLE === false) { uxLog(this, c.yellow("MermaidJs is not available. To improve performances, please install it by running `npm install @mermaid-js/mermaid-cli --global`")); } return IS_MERMAID_AVAILABLE; } let IS_DOCKER_AVAILABLE = null; export async function isDockerAvailable() { if (IS_DOCKER_AVAILABLE !== null) { return IS_DOCKER_AVAILABLE; } IS_DOCKER_AVAILABLE = await isDockerRunning(); if (!IS_DOCKER_AVAILABLE) { uxLog(this, c.yellow("Docker daemon is not available. If you have issues running npm package @mermaid-js/mermaid-cli, please install Docker and start it")); } return IS_DOCKER_AVAILABLE; } export async function generateFlowMarkdownFile(flowName, flowXml, outputFlowMdFile, options = { collapsedDetails: true, describeWithAi: true, flowDependencies: {} }) { try { const flowDocGenResult = await parseFlow(flowXml, 'mermaid', { outputAsMarkdown: true, collapsedDetails: options.collapsedDetails }); let flowMarkdownDoc = flowDocGenResult.uml; if (options.describeWithAi) { const docBuilder = new DocBuilderFlow(flowName, flowXml, ""); docBuilder.markdownDoc = flowMarkdownDoc; flowMarkdownDoc = await docBuilder.completeDocWithAiDescription(); } // Add link to history flow doc const historyFlowDoc = path.join("docs", "flows", flowName + "-history.md"); if (fs.existsSync(historyFlowDoc)) { const historyLink = `[(_View History_)](${flowName + "-history.md"})`; if (flowMarkdownDoc.includes("## Flow Diagram") && !flowMarkdownDoc.includes(historyLink)) { flowMarkdownDoc = flowMarkdownDoc.replace("## Flow Diagram", `## Flow Diagram ${historyLink}`); } } // Add flow dependencies const dependencies = []; for (const mainFlow of Object.keys(options.flowDependencies)) { if (options.flowDependencies[mainFlow].includes(flowName)) { dependencies.push(mainFlow); } } if (dependencies.length > 0) { flowMarkdownDoc += `\n\n## Dependencies\n\n${dependencies.map(dep => `- [${dep}](${dep}.md)`).join("\n")}\n`; } await fs.writeFile(outputFlowMdFile, flowMarkdownDoc); uxLog(this, c.grey(`Written ${flowName} documentation in ${outputFlowMdFile}`)); return true; } catch (e) { uxLog(this, c.yellow(`Error generating Flow ${flowName} documentation: ${e.message}`) + "\n" + c.grey(e.stack)); return false; } } export async function generateMarkdownFileWithMermaid(outputFlowMdFileIn, outputFlowMdFileOut, mermaidModes = null, withPdf = false) { await fs.ensureDir(path.dirname(outputFlowMdFileIn)); await fs.ensureDir(path.dirname(outputFlowMdFileOut)); if (withPdf) { // Force the usage of mermaid CLI so the mermaid code is converted to SVG mermaidModes = ["cli"]; } else if (process.env.MERMAID_MODES) { mermaidModes = process.env.MERMAID_MODES.split(","); } else if (mermaidModes === null) { mermaidModes = ["mermaid", "cli", "docker"]; } if (mermaidModes.includes("mermaid")) { return true; } const isDockerAvlbl = await isDockerAvailable(); if (isDockerAvlbl && (!(globalThis.mermaidUnavailableTools || []).includes("docker")) && mermaidModes.includes("docker")) { const dockerSuccess = await generateMarkdownFileWithMermaidDocker(outputFlowMdFileIn, outputFlowMdFileOut); if (dockerSuccess) { return true; } } if ((!(globalThis.mermaidUnavailableTools || []).includes("cli")) && mermaidModes.includes("cli")) { const mmCliSuccess = await generateMarkdownFileWithMermaidCli(outputFlowMdFileIn, outputFlowMdFileOut); if (mmCliSuccess) { if (withPdf) { const pdfGenerated = await generatePdfFileFromMarkdown(outputFlowMdFileOut); if (!pdfGenerated) { return false; } const fileName = path.basename(pdfGenerated).replace(".pdf", ""); uxLog(this, c.grey(`Written ${fileName} PDF documentation in ${pdfGenerated}`)); } return true; } } if ((globalThis.mermaidUnavailableTools || []).includes("cli") && (globalThis.mermaidUnavailableTools || []).includes("docker")) { uxLog(this, c.yellow("Either mermaid-cli or docker is required to work to generate mermaidJs Graphs. Please install/fix one of them if you want to generate SVG diagrams.")); } return false; } export async function generateMarkdownFileWithMermaidDocker(outputFlowMdFileIn, outputFlowMdFileOut) { const fileDir = path.resolve(path.dirname(outputFlowMdFileIn)); const fileName = path.basename(outputFlowMdFileIn); const fileOut = path.basename(outputFlowMdFileOut); const dockerCommand = `docker run --rm -v "${fileDir}:/data" ghcr.io/mermaid-js/mermaid-cli/mermaid-cli -i "${fileName}" -o "${fileOut}"`; try { await execCommand(dockerCommand, this, { output: false, fail: true, debug: false }); return true; } catch (e) { uxLog(this, c.yellow(`Error generating mermaidJs Graphs from ${outputFlowMdFileIn} documentation with Docker: ${e.message}`) + "\n" + c.grey(e.stack)); if (JSON.stringify(e).includes("Cannot connect to the Docker daemon") || JSON.stringify(e).includes("daemon is not running")) { globalThis.mermaidUnavailableTools = (globalThis.mermaidUnavailableTools || []).concat("docker"); uxLog(this, c.yellow("[Mermaid] Docker unavailable: do not try again")); } return false; } } export async function generateMarkdownFileWithMermaidCli(outputFlowMdFileIn, outputFlowMdFileOut) { // Try with NPM package const isMmdAvailable = await isMermaidAvailable(); const puppeteerConfigPath = path.join(PACKAGE_ROOT_DIR, 'defaults', 'puppeteer-config.json'); const mermaidCmd = `${!isMmdAvailable ? 'npx --yes -p @mermaid-js/mermaid-cli ' : ''}mmdc -i "${outputFlowMdFileIn}" -o "${outputFlowMdFileOut}" --puppeteerConfigFile "${puppeteerConfigPath}"`; try { await execCommand(mermaidCmd, this, { output: false, fail: true, debug: false }); return true; } catch (e) { uxLog(this, c.yellow(`Error generating mermaidJs Graphs from ${outputFlowMdFileIn} documentation with CLI: ${e.message}`) + "\n" + c.grey(e.stack)); if (JSON.stringify(e).includes("timed out")) { globalThis.mermaidUnavailableTools = (globalThis.mermaidUnavailableTools || []).concat("cli"); uxLog(this, c.yellow("[Mermaid] CLI unavailable: do not try again")); } return false; } } export function getMermaidExtraClasses() { const added = 'fill:green,color:white,stroke-width:4px,text-decoration:none,max-height:100px'; const removed = 'fill:red,color:white,stroke-width:4px,text-decoration:none,max-height:100px'; const changed = 'fill:orange,color:white,stroke-width:4px,text-decoration:none,max-height:100px'; const addedClasses = [ 'actionCallsAdded', 'assignmentsAdded', 'collectionProcessorsAdded', 'customErrorsAdded', 'decisionsAdded', 'loopsAdded', 'recordCreatesAdded', 'recordDeletesAdded', 'recordLookupsAdded', 'recordUpdatesAdded', 'screensAdded', 'subflowsAdded', 'startClassAdded', 'transformsAdded' ]; const removedClasses = [ 'actionCallsRemoved', 'assignmentsRemoved', 'collectionProcessorsRemoved', 'customErrorsRemoved', 'decisionsRemoved', 'loopsRemoved', 'recordCreatesRemoved', 'recordDeletesRemoved', 'recordLookupsRemoved', 'recordUpdatesRemoved', 'screensRemoved', 'subflowsRemoved', 'startClassRemoved', 'transformsRemoved' ]; const changedClasses = [ 'actionCallsChanged', 'assignmentsChanged', 'collectionProcessorsChanged', 'customErrorsChanged', 'decisionsChanged', 'loopsChanged', 'recordCreatesChanged', 'recordDeletesChanged', 'recordLookupsChanged', 'recordUpdatesChanged', 'screensChanged', 'subflowsChanged', 'startClassChanged', 'transformsChanged' ]; const formatClasses = (classList, style) => classList.map(className => `classDef ${className} ${style}`).join('\n'); return ` ${formatClasses(addedClasses, added)} ${formatClasses(removedClasses, removed)} ${formatClasses(changedClasses, changed)} `; } export async function generateFlowVisualGitDiff(flowFile, commitBefore, commitAfter, options = { mermaidMd: false, svgMd: true, pngMd: false, debug: false }) { const result = { outputDiffMdFile: "", hasFlowDiffs: false }; const { mermaidMdBefore, flowXmlBefore } = await getFlowXmlBefore(commitBefore, flowFile); const { mermaidMdAfter, flowXmlAfter } = await getFlowXmlAfter(commitAfter, flowFile); const flowLabel = path.basename(flowFile, ".flow-meta.xml"); const reportDir = await getReportDirectory(); await fs.ensureDir(path.join(reportDir, "flow-diff")); const diffMdFile = path.join(reportDir, 'flow-diff', `${flowLabel}_${moment().format("YYYYMMDD-hhmmss")}.md`); if (options.debug) { uxLog(this, c.grey("FLOW DOC BEFORE:\n" + mermaidMdBefore) + "\n"); await fs.writeFile(diffMdFile.replace(".md", ".mermaid-before.md"), mermaidMdBefore); uxLog(this, c.grey("FLOW DOC AFTER:\n" + mermaidMdAfter) + "\n"); await fs.writeFile(diffMdFile.replace(".md", ".mermaid-after.md"), mermaidMdAfter); } const flowDiffs = Diff.diffLines(mermaidMdBefore, mermaidMdAfter); result.hasFlowDiffs = flowDiffs.some((line) => (line.added || line.removed) && line.value.trim() !== ""); result.diffLines = flowDiffs.filter(line => line.added || line.removed); const mixedLines = []; for (const line of flowDiffs) { if (line.added) { mixedLines.push(...line.value.split(/\r?\n/).map(lineSplit => { return ["added", lineSplit]; })); } else if (line.removed) { mixedLines.push(...line.value.split(/\r?\n/).map(lineSplit => { return ["removed", lineSplit]; })); } else { mixedLines.push(...line.value.split(/\r?\n/).map(lineSplit => { return ["unchanged", lineSplit]; })); } } // uxLog(this, JSON.stringify(mixedLines, null, 2)); const compareMdLines = []; const linkLines = []; buildFinalCompareMarkdown(mixedLines, compareMdLines, false, false, linkLines); let diffMarkdown = compareMdLines.join("\n"); if (result.hasFlowDiffs === true && flowXmlAfter !== "" && flowXmlBefore !== "") { const flowDiffKey = `${flowLabel}-${commitBefore}-${commitAfter}`; diffMarkdown = await completeWithDiffAiDescription(diffMarkdown, flowXmlAfter, flowXmlBefore, flowDiffKey); } // Write markdown with diff in a file await fs.writeFile(diffMdFile, diffMarkdown); if (options.mermaidMd) { await fs.copyFile(diffMdFile, diffMdFile.replace(".md", ".mermaid.md")); } result.outputDiffMdFile = diffMdFile; if (!options.svgMd && !options.pngMd) { return result; } if (options.svgMd) { // Generate final markdown with mermaid SVG const finalRes = await generateMarkdownFileWithMermaid(diffMdFile, diffMdFile, ["cli", "docker"]); if (finalRes) { uxLog(this, c.green(`Successfully generated visual git diff for flow: ${diffMdFile}`)); } } else if (options.pngMd) { // General final markdown with mermaid PNG const pngFile = path.join(path.dirname(diffMdFile), path.basename(diffMdFile, ".md") + ".png"); const pngRes = await generateMarkdownFileWithMermaid(diffMdFile, pngFile, ["cli", "docker"]); if (pngRes) { let mdWithMermaid = fs.readFileSync(diffMdFile, "utf8"); mdWithMermaid = mdWithMermaid.replace(/```mermaid\n([\s\S]*?)\n```/g, `![Diagram as PNG](./${path.basename(pngFile).replace(".png", "-1.png")})`); await fs.writeFile(diffMdFile, mdWithMermaid); } } return result; } async function getFlowXmlAfter(commitAfter, flowFile) { try { const flowXmlAfter = await git().show([`${commitAfter}:${flowFile}`]); const mermaidMdAfter = await buildMermaidMarkdown(flowXmlAfter, flowFile); return { mermaidMdAfter, flowXmlAfter }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars catch (err) { return { mermaidMdAfter: "", flowXmlAfter: "" }; } } async function getFlowXmlBefore(commitBefore, flowFile) { try { const flowXmlBefore = await git().show([`${commitBefore}:${flowFile}`]); const mermaidMdBefore = await buildMermaidMarkdown(flowXmlBefore, flowFile); return { mermaidMdBefore, flowXmlBefore }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars catch (err) { return { mermaidMdBefore: "", flowXmlBefore: "" }; } } function buildFinalCompareMarkdown(mixedLines, compareMdLines, isMermaid, isTableStarted, linkLines) { if (mixedLines.length === 0) { return; } // Take line to process const [status, currentLine] = mixedLines.shift(); // Update mermaid state if (isMermaid === false && currentLine.includes("```mermaid")) { isMermaid = true; } else if (isMermaid === true && currentLine.includes("```")) { compareMdLines.push(...getMermaidExtraClasses().split("\n")); // Build link positions let pos = 0; const positions = { added: [], removed: [], unchanged: [] }; for (const linkType of linkLines) { positions[linkType].push(pos); pos++; } // Build added and removed links styles if (positions.added.length > 0) { compareMdLines.push("linkStyle " + positions.added.join(",") + " stroke:#00ff00,stroke-width:4px,color:green;"); } if (positions.removed.length > 0) { compareMdLines.push("linkStyle " + positions.removed.join(",") + " stroke:#ff0000,stroke-width:4px,color:red;"); } isMermaid = false; } let styledLine = currentLine; // Remove next diff line if not relevant if (styledLine.startsWith("|") && mixedLines.length > 1 && mixedLines[0][1] === '' && mixedLines[1][1].startsWith("|") && !mixedLines[1][1].startsWith("|Condition Id|") && !mixedLines[1][1].startsWith("|Filter Id|")) { mixedLines.shift(); } // Skip table block if there are no updated lines within if (styledLine.startsWith("## ") && !styledLine.startsWith("## Flow Diagram")) { let updatedInBlock = false; let nextBlockPos = 0; for (const nextLine of mixedLines) { if (nextLine[1].startsWith("## ") || nextLine[1].includes("_Documentation") || nextLine[1].startsWith("___")) { break; } if (nextLine[0] === "removed" || nextLine[0] === "added") { updatedInBlock = true; } nextBlockPos++; } if (!updatedInBlock) { const mixedLinesStartingFromNextBlock = mixedLines.slice(nextBlockPos); // Continue processing next lines buildFinalCompareMarkdown(mixedLinesStartingFromNextBlock, compareMdLines, isMermaid, false, linkLines); return; } } /* jscpd:ignore-start */ // Skip node block if there are no updated lines within else if (styledLine.startsWith("### ")) { let updatedInBlock = false; let nextBlockPos = 0; for (const nextLine of mixedLines) { if (nextLine[1].startsWith("### ") || nextLine[1].startsWith("## ") || nextLine[1].includes("_Documentation") || nextLine[1].startsWith("___")) { break; } if (nextLine[0] === "removed" || nextLine[0] === "added") { updatedInBlock = true; } nextBlockPos++; } if (!updatedInBlock) { const mixedLinesStartingFromNextBlock = mixedLines.slice(nextBlockPos); // Continue processing next lines buildFinalCompareMarkdown(mixedLinesStartingFromNextBlock, compareMdLines, isMermaid, false, linkLines); return; } } else if (styledLine.startsWith("#### ")) { let updatedInBlock = false; let nextBlockPos = 0; for (const nextLine of mixedLines) { if (nextLine[1].startsWith("#### ") || nextLine[1].startsWith("### ") || nextLine[1].startsWith("## ") || nextLine[1].includes("_Documentation") || nextLine[1].startsWith("___")) { break; } if (nextLine[0] === "removed" || nextLine[0] === "added") { updatedInBlock = true; } nextBlockPos++; } if (!updatedInBlock) { const mixedLinesStartingFromNextBlock = mixedLines.slice(nextBlockPos); // Continue processing next lines buildFinalCompareMarkdown(mixedLinesStartingFromNextBlock, compareMdLines, isMermaid, false, linkLines); return; } } /* jscpd:ignore-end */ // Skip table lines that have not been updated /* else if (!isMermaid && styledLine.startsWith("|") && isTableStarted === false) { isTableStarted = true; const tableFilteredLines: any[] = []; let endTablePos = 0; for (const nextLine of mixedLines) { if ((!nextLine[1].startsWith("|") || nextLine[1].includes("Condition Id") || nextLine[1].includes("Filter Id")) && nextLine[1] !== "") { break; } if ((nextLine[0] === "removed" || nextLine[0] === "added" || endTablePos === 0) && nextLine[1] !== "") { tableFilteredLines.push(nextLine); } endTablePos++; } if (tableFilteredLines.length < 2) { // Empty table const mixedLinesStartingFromEndOfTable = mixedLines.slice(endTablePos); buildFinalCompareMarkdown(mixedLinesStartingFromEndOfTable, compareMdLines, isMermaid, false, linkLines); } else { compareMdLines.push(styledLine); const mixedLinesStartingFromEndOfTable = mixedLines.slice(endTablePos); const newMixedLines = [...tableFilteredLines, ...[["unchanged", ""]], ...mixedLinesStartingFromEndOfTable]; // Continue processing next lines buildFinalCompareMarkdown(newMixedLines, compareMdLines, isMermaid, true, linkLines); } return; } */ // Tables lines if (!isMermaid && status === "removed" && styledLine.startsWith("|") && !styledLine.startsWith("|:-")) { styledLine = "|🟥" + styledLine.split("|").filter(e => e !== "").map((col) => `<span style="background-color: #ff7f7f; color: black;"><i>${col}</i></span>`).join("|") + "|"; } else if (!isMermaid && status === "added" && styledLine.startsWith("|") && !styledLine.startsWith("|:-")) { styledLine = "|🟩" + styledLine.split("|").filter(e => e !== "").map((col) => `<span style="background-color: #a6e22e; color: black;"><b>${col}</b></span>`).join("|") + "|"; } // Normal lines header 3 else if (!isMermaid && status === "removed" && styledLine.startsWith("#### ")) { styledLine = `#### 🟥${styledLine.replace("#### ", "")}`; } else if (!isMermaid && status === "added" && styledLine.startsWith("#### ")) { styledLine = `#### 🟩${styledLine.replace("#### ", "")}`; } // Normal lines header 2 else if (!isMermaid && status === "removed" && styledLine.startsWith("### ")) { styledLine = `### 🟥${styledLine.replace("### ", "")}`; } else if (!isMermaid && status === "added" && styledLine.startsWith("### ")) { styledLine = `### 🟩${styledLine.replace("### ", "")}`; } // Normal lines header 3 else if (!isMermaid && status === "removed" && styledLine.startsWith("## ")) { styledLine = `## 🟥${styledLine.replace("## ", "")}`; } else if (!isMermaid && status === "added" && styledLine.startsWith("## ")) { styledLine = `## 🟩${styledLine.replace("## ", "")}`; } // Normal lines else if (!isMermaid && status === "removed" && styledLine !== "" && !styledLine.includes('```') && !styledLine.startsWith("|:-") && !styledLine.startsWith("___")) { styledLine = `<span style="background-color: #ff7f7f; color: black;"><i>🟥${styledLine}</i></span>`; } else if (!isMermaid && status === "added" && styledLine !== "" && !styledLine.includes('```') && !styledLine.startsWith("|:-") && !styledLine.startsWith("___")) { styledLine = `<span style="background-color: #a6e22e; color: black;"><b>🟩${styledLine}</b></span>`; } // Boxes lines else if (isMermaid === true && status === "removed" && currentLine.split(":::").length === 2) { styledLine = styledLine + "Removed"; if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); styledLine = splits[0] + '"<i>' + splits[1] + '</i>"' + splits[2]; } } else if (isMermaid === true && status === "added" && currentLine.split(":::").length === 2) { styledLine = styledLine + "Added"; if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); styledLine = splits[0] + '"<b>' + splits[1] + '</b>"' + splits[2]; } } else if (isMermaid === true && currentLine.includes(":::")) { // Detect if link line does not change, but its content did const splits = currentLine.split(/[[({]/); if (splits.length > 1) { const boxName = splits[0]; const changed = mixedLines.filter(([lineStatus, line]) => { return line.startsWith(`click ${boxName}`) && ["added", "removed"].includes(lineStatus); }).length; if (changed > 0) { styledLine = styledLine + "Changed"; if (styledLine.split('"').length === 3) { const splits = styledLine.split('"'); styledLine = splits[0] + '"<b>' + splits[1] + '</b>"' + splits[2]; } // Remove "removed" line from mixedLines const removedNodePos = mixedLines.findIndex(([lineStatus, line]) => { return line.startsWith(`click ${boxName}`) && lineStatus === "removed"; }); if (removedNodePos !== -1) { mixedLines.splice(removedNodePos, 1); } } } } // Long Link lines else if (isMermaid === true && status === "removed" && currentLine.includes('-. Fault .->')) { styledLine = styledLine.replace('-. Fault .->', '-. 🟥Fault .->'); //+ ":::removedLink" linkLines.push("removed"); } else if (isMermaid === true && status === "added" && currentLine.includes('-. Fault .->')) { styledLine = styledLine.replace('-. Fault .->', '-. 🟩Fault .->'); // + ":::addedLink" linkLines.push("added"); } /* jscpd:ignore-start */ // Long Link lines else if (isMermaid === true && status === "removed" && currentLine.includes('--->')) { styledLine = styledLine.replace("--->", "-.->"); //+ ":::removedLink" linkLines.push("removed"); if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); styledLine = splits[0] + '|"🟥<i>' + removeQuotes(splits[1]) + '</i>"|' + splits[2]; } } else if (isMermaid === true && status === "added" && currentLine.includes('--->')) { styledLine = styledLine.replace("--->", "===>"); // + ":::addedLink" linkLines.push("added"); if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); styledLine = splits[0] + '|"🟩<b>' + removeQuotes(splits[1]) + '</b>"|' + splits[2]; } } // Link lines else if (isMermaid === true && status === "removed" && currentLine.includes('-->')) { styledLine = styledLine.replace("-->", "-.->"); // + ":::removedLink" linkLines.push("removed"); if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); styledLine = splits[0] + '|"🟥<i>' + removeQuotes(splits[1]) + '</i>"|' + splits[2]; } } else if (isMermaid === true && status === "added" && currentLine.includes('-->')) { styledLine = styledLine.replace("-->", "==>"); // + ":::addedLink" linkLines.push("added"); if (styledLine.split("|").length === 3) { const splits = styledLine.split("|"); styledLine = splits[0] + '|"🟩<b>' + removeQuotes(splits[1]) + '</b>"|' + splits[2]; } } else if (isMermaid === true && !["added", "removed"].includes(status) && (currentLine.includes('-->') || currentLine.includes('-. Fault .->'))) { linkLines.push("unchanged"); } /* jscpd:ignore-end */ compareMdLines.push(styledLine); // Continue processing next lines buildFinalCompareMarkdown(mixedLines, compareMdLines, isMermaid, (styledLine.startsWith("|") && isTableStarted), linkLines); } async function buildMermaidMarkdown(flowXml, flowFile) { try { const flowDocGenResult = await parseFlow(flowXml, 'mermaid', { outputAsMarkdown: true }); return flowDocGenResult.uml; } catch (err) { throw new SfError(`Unable to build Graph for flow ${flowFile}: ${err.message}`); } } function removeQuotes(str) { if (str.startsWith('"')) { str = str.slice(1); } if (str.endsWith('"')) { str = str.slice(0, -1); } return str; } export async function generateHistoryDiffMarkdown(flowFile, debugMode) { await fs.ensureDir(path.join("docs", "flows")); const diffMdFile = path.join("docs", "flows", path.basename(flowFile).replace(".flow-meta.xml", "-history.md")); // Compute for all states const fileHistory = await git().log({ file: flowFile }); const flowLabel = path.basename(flowFile, ".flow-meta.xml"); uxLog(this, c.cyan(`Generating ${flowLabel} markdown diff between ${fileHistory.all.length} Flow states...`)); const diffMdFiles = []; for (let i = 0; i < fileHistory.all.length; i++) { const commitAfter = fileHistory.all[i]; // Initial state if (i === fileHistory.all.length - 1) { const flowXml = await git().show([`${fileHistory.all[i].hash}:${flowFile}`]); const reportDir = await getReportDirectory(); await fs.ensureDir(path.join(reportDir, "flow-diff")); const diffMdFileTmp = path.join(reportDir, 'flow-diff', `${flowLabel}_${moment().format("YYYYMMDD-hhmmss")}.md`); const genRes = await generateFlowMarkdownFile(flowLabel, flowXml, diffMdFileTmp, { collapsedDetails: false, describeWithAi: false, flowDependencies: {} }); if (!genRes) { throw new Error(`Error generating markdown file for flow ${flowFile}`); } diffMdFiles.push({ initialVersion: true, commitAfter: commitAfter, markdown: fs.readFileSync(diffMdFileTmp, "utf8") }); } else { const commitBefore = fileHistory.all[i + 1]; const genDiffRes = await generateFlowVisualGitDiff(flowFile, commitBefore.hash, commitAfter.hash, { svgMd: false, mermaidMd: true, pngMd: false, debug: debugMode }); if (genDiffRes.hasFlowDiffs && fs.existsSync(genDiffRes.outputDiffMdFile)) { diffMdFiles.push({ commitBefore: commitBefore, commitAfter: commitAfter, markdown: fs.readFileSync(genDiffRes.outputDiffMdFile, "utf8") }); } else { uxLog(this, c.yellow(`No real flow diff has been found between ${commitBefore.hash} and ${commitAfter.hash}`)); } } } // Set all the results in a single tabbed markdown uxLog(this, c.cyan(`Aggregating results in summary tabbed file ${diffMdFile}...`)); let finalMd = `# ${flowLabel} history\n\n`; finalMd += "<!-- This page has been generated to be viewed with mkdocs-material, you can not view it just as markdown . Activate tab plugin following the doc at https://squidfunk.github.io/mkdocs-material/reference/content-tabs/ -->\n\n"; for (const diffMdFile of diffMdFiles) { finalMd += `=== "${moment(diffMdFile.commitAfter.date).format("ll")}` + (diffMdFile.initialVersion ? " (Initial)" : "") + `"\n\n`; finalMd += ` _${moment(diffMdFile.commitAfter.date).format("ll")}, by ${diffMdFile.commitAfter.author_name} in commit ${diffMdFile.commitAfter.message}_\n\n`; // Remove title and add indentation for tabs to be displayed finalMd += diffMdFile.markdown.split("\n").filter(line => !line.startsWith("# ")).map(line => ` ${line}`).join("\n"); finalMd += "\n\n"; } await fs.writeFile(diffMdFile, finalMd); if (debugMode) { await fs.copyFile(diffMdFile, diffMdFile.replace(".md", ".mermaid.md")); } const genSvgRes = await generateMarkdownFileWithMermaid(diffMdFile, diffMdFile); if (!genSvgRes) { throw new Error("Error generating mermaid markdown file"); } // Fix indentation for mermaid SVG links const diffMarkdown = await fs.readFile(diffMdFile, "utf8"); const diffMarkdownFixed = diffMarkdown.split("\n").map(line => { if (line.startsWith("![diagram]")) { return ` ${line}`; } return line; }).join("\n"); await fs.writeFile(diffMdFile, diffMarkdownFixed); // Add link to main flow doc const mainFlowDoc = path.join("docs", "flows", path.basename(flowFile).replace(".flow-meta.xml", ".md")); if (fs.existsSync(mainFlowDoc)) { const mainFlowDocContent = await fs.readFile(mainFlowDoc, "utf8"); const mainFlowDocLink = `[(_View History_)](${path.basename(flowFile).replace(".flow-meta.xml", "-history.md")})`; if (mainFlowDocContent.includes("## Flow Diagram") && !mainFlowDocContent.includes(mainFlowDocLink)) { const updatedFlowDocContent = mainFlowDocContent.replace("## Flow Diagram", `## Flow Diagram ${mainFlowDocLink}`); await fs.writeFile(mainFlowDoc, updatedFlowDocContent); } } uxLog(this, c.green(`Markdown diff between ${fileHistory.all.length} Flow states generated in ${diffMdFile}`)); return diffMdFile; } export function removeMermaidLinks(messageBody) { let result = messageBody + ""; if (result.includes("```mermaid")) { let withinMermaid = false; result = result .split("\n") .filter((line) => { // Toggle mermaid flag on/off if (line.includes("```mermaid")) { withinMermaid = true; } else if (line.includes("```") && withinMermaid === true) { withinMermaid = false; } // Filter if click line for better display if (line.startsWith("click") && withinMermaid === true) { return false; } return true; }) .join("\n"); } return result; } /* jscpd:ignore-start */ async function completeWithDiffAiDescription(flowMarkdownDoc, flowXmlNew, flowXmlPrevious, diffKey) { const flowXmlNewStripped = await new DocBuilderFlow("", flowXmlNew, "").stripXmlForAi(); const flowXmlPreviousStripped = await new DocBuilderFlow("", flowXmlPrevious, "").stripXmlForAi(); const aiCache = await UtilsAi.findAiCache("PROMPT_DESCRIBE_FLOW_DIFF", [flowXmlNewStripped, flowXmlPreviousStripped], diffKey); if (aiCache.success) { uxLog(this, c.grey("Used AI cache for diff description (set IGNORE_AI_CACHE=true to force call to AI)")); const replaceText = `## AI-Generated Differences Summary\n\n${includeFromFile(aiCache.aiCacheDirFile, aiCache.cacheText || "")}`; return flowMarkdownDoc.replace("<!-- Flow description -->", replaceText); } if (AiProvider.isAiAvailable()) { // Invoke AI Service const prompt = AiProvider.buildPrompt("PROMPT_DESCRIBE_FLOW_DIFF", { "FLOW_XML_NEW": flowXmlNewStripped, "FLOW_XML_PREVIOUS": flowXmlPreviousStripped }); const aiResponse = await AiProvider.promptAi(prompt, "PROMPT_DESCRIBE_FLOW_DIFF"); // Replace description in markdown if (aiResponse?.success) { let responseText = aiResponse.promptResponse || "No AI description available"; if (responseText.startsWith("##")) { responseText = responseText.split("\n").slice(1).join("\n"); } await UtilsAi.writeAiCache("PROMPT_DESCRIBE_FLOW_DIFF", [flowXmlNewStripped, flowXmlPreviousStripped], diffKey, responseText); const replaceText = `## AI-Generated Differences Summary\n\n${includeFromFile(aiCache.aiCacheDirFile, responseText || "")}`; const flowMarkdownDocUpdated = flowMarkdownDoc.replace("<!-- Flow description -->", replaceText); return flowMarkdownDocUpdated; } } return flowMarkdownDoc; } /* jscpd:ignore-end */ //# sourceMappingURL=mermaidUtils.js.map