From 3731fa0c7e45697a742c83a1b572b0061b196ffd Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Sat, 4 Jan 2025 02:16:33 +0100 Subject: [PATCH] mkdocs-to-salesforce v0 --- .../hardis/doc/mkdocs-to-salesforce.ts | 183 ++++++++++++++++++ src/common/utils/flowVisualiser/flowParser.ts | 2 +- src/common/utils/mermaidUtils.ts | 6 +- 3 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 src/commands/hardis/doc/mkdocs-to-salesforce.ts diff --git a/src/commands/hardis/doc/mkdocs-to-salesforce.ts b/src/commands/hardis/doc/mkdocs-to-salesforce.ts new file mode 100644 index 000000000..608fcc09f --- /dev/null +++ b/src/commands/hardis/doc/mkdocs-to-salesforce.ts @@ -0,0 +1,183 @@ +/* jscpd:ignore-start */ +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import fs, { ensureDir } from 'fs-extra'; +import c from "chalk"; +import * as path from "path"; +import { Messages, SfError } from '@salesforce/core'; +import { AnyJson } from '@salesforce/ts-types'; +import { createTempDir, execCommand, uxLog } from '../../../common/utils/index.js'; +import { createBlankSfdxProject } from '../../../common/utils/projectUtils.js'; +import { isProductionOrg } from '../../../common/utils/orgUtils.js'; + + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('sfdx-hardis', 'org'); + +export default class MkDocsToSalesforce extends SfCommand { + public static title = 'MkDocs to Salesforce'; + + public static description = `Generates MkDocs HTML pages and upload them to Salesforce as a static resource +`; + + public static examples = [ + '$ sf hardis:doc:mkdocs-to-salesforce', + ]; + + public static flags: any = { + type: Flags.string({ + char: 't', + options: ["CICD", "Monitoring"], + default: "CICD", + description: 'Type of the documentation to generate. Default is "all"', + }), + debug: Flags.boolean({ + char: 'd', + default: false, + description: messages.getMessage('debugMode'), + }), + websocket: Flags.string({ + description: messages.getMessage('websocket'), + }), + skipauth: Flags.boolean({ + description: 'Skip authentication check when a default username is required', + }), + }; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + public static requiresProject = true; + + protected debugMode = false; + /* jscpd:ignore-end */ + + public async run(): Promise { + const { flags } = await this.parse(MkDocsToSalesforce); + const type = flags.type || "CICD"; + const targetUsername = flags['target-org'].getUsername(); + const conn = flags['target-org'].getConnection(); + this.debugMode = flags.debug || false; + + // Check if the project is a MkDocs project + const mkdocsYmlPath = path.join(process.cwd(), "mkdocs.yml"); + if (!fs.existsSync(mkdocsYmlPath)) { + throw new SfError('This command needs a mkdocs.yml config file. Generate one using "sf hardis:doc:project2markdown --with-history"'); + } + + // Install Python dependencies + const mkdocsLocalOk = await this.installMkDocs(); + + await this.generateMkDocsHTML(mkdocsLocalOk); + + // Create temp sfdx project + const tmpDirForSfdxProject = await createTempDir(); + const tmpProjectPath = await createBlankSfdxProject(tmpDirForSfdxProject); + const defaultProjectPath = path.join(tmpProjectPath, "force-app", "main", "default"); + + // Create static resource folder + const resName = `SfdxHardisMkDocsSite-${type}`; + const { mkDocsResourcePath, vfPageMetaFile, tabMetaFile } = await this.createDocMetadatas(resName, defaultProjectPath, type); + + // Upload resource to remote org + const deployRes = await this.uploadDocMetadatas(resName, targetUsername, conn, tmpProjectPath, mkDocsResourcePath, vfPageMetaFile, tabMetaFile); + + return { success: deployRes.status === 0 }; + } + + private async installMkDocs() { + uxLog(this, c.cyan("Managing mkdocs-material local installation...")); + let mkdocsLocalOk = false; + const installMkDocsRes = await execCommand("pip install mkdocs-material mdx_truly_sane_lists", this, { fail: false, output: true, debug: this.debugMode }); + if (installMkDocsRes.status !== 0) { + mkdocsLocalOk = true; + } + return mkdocsLocalOk; + } + + private async generateMkDocsHTML(mkdocsLocalOk: boolean) { + if (mkdocsLocalOk) { + // Generate MkDocs HTML pages with local MkDocs + uxLog(this, c.cyan("Generating HTML pages with mkdocs...")); + const mkdocsBuildRes = await execCommand("mkdocs build", this, { fail: false, output: true, debug: this.debugMode }); + if (mkdocsBuildRes.status !== 0) { + throw new SfError('MkDocs build failed:\n' + mkdocsBuildRes.stderr + "\n" + mkdocsBuildRes.stdout); + } + } + } + + private async createDocMetadatas(resName: string, defaultProjectPath: string, type: any) { + uxLog(this, c.cyan(`Creating Static Resource ${resName} metadata...`)); + const staticResourcePath = path.join(defaultProjectPath, "staticresources"); + const mkDocsResourcePath = path.join(staticResourcePath, resName); + await ensureDir(mkDocsResourcePath); + await fs.move(path.join(process.cwd(), "site"), mkDocsResourcePath, { overwrite: true }); + + // Create Static resource metadata + uxLog(this, c.cyan(`Creating Static Resource ${resName} metadata...`)); + const mkDocsResourceFileName = path.join(staticResourcePath, `${resName}.resource-meta.xml`); + const mkDocsResourceMeta = ` + + Private + application/x-zip-compressed + +`; + await fs.writeFile(mkDocsResourceFileName, mkDocsResourceMeta); + + // Create visual force page + uxLog(this, c.cyan(`Creating VisualForce page ${resName} metadata...`)); + const vfPagesPath = path.join(defaultProjectPath, "pages"); + const vfPageFileName = path.join(vfPagesPath, `${resName}.page`); + const vfPageCode = ` + + +`; + await fs.writeFile(vfPageFileName, vfPageCode); + + // Create visual force page metadata + const vfPageMetaFile = path.join(vfPagesPath, `${resName}.page-meta.xml`); + const vfPageMeta = ` + + 62.0 + false + false + + +`; + await fs.writeFile(vfPageMetaFile, vfPageMeta); + + // Create custom tab metadata + const tabsPath = path.join(defaultProjectPath, "tabs"); + const tabMetaFile = path.join(tabsPath, `${resName}.tab-meta.xml`); + const tabMeta = ` + + + Custom46: Postage + ${resName} + +`; + await fs.writeFile(tabMetaFile, tabMeta); + return { mkDocsResourcePath, vfPageMetaFile, tabMetaFile }; + } + + private async uploadDocMetadatas(resName: string, targetUsername: any, conn: any, tmpProjectPath: string, mkDocsResourcePath: string, vfPageMetaFile: string, tabMetaFile: string) { + let deployCommand = `sf project deploy start -m StaticResource:${resName} -m ApexPage:${resName} -m CustomTab:${resName}`; + const isProdOrg = await isProductionOrg(targetUsername, { conn: conn }); + if (isProdOrg) { + deployCommand += " --test-level RunLocalTests"; + } + else { + deployCommand += " --test-level NoTestRun"; + } + const deployRes = await execCommand(deployCommand, this, { cwd: tmpProjectPath, fail: false, output: true, debug: this.debugMode }); + + if (deployRes.status !== 0) { + uxLog(this, c.red(`Deployment failed:\n${deployRes.stderr + "\n" + deployRes.stdout}`)); + uxLog(this, c.yellow(`You can manually deploy the Static Resource ${resName},the VisualForce page ${resName} and the custom tab ${resName} to your org`)); + uxLog(this, c.yellow(`- Static Resource: ${mkDocsResourcePath} (If you upload using UI, zip the folder and make sure to have index.html at the zip root)`)); + uxLog(this, c.yellow(`- VisualForce page: ${vfPageMetaFile}`)); + uxLog(this, c.yellow(`- Custom tab: ${tabMetaFile}`)); + } + else { + uxLog(this, c.green(`SFDX Project documentation uploaded to salesforce and available in Custom Tab ${resName}`)); + } + return deployRes; + } +} diff --git a/src/common/utils/flowVisualiser/flowParser.ts b/src/common/utils/flowVisualiser/flowParser.ts index f995b68cf..3bfa14b71 100644 --- a/src/common/utils/flowVisualiser/flowParser.ts +++ b/src/common/utils/flowVisualiser/flowParser.ts @@ -447,7 +447,7 @@ function getTemplatesMd(textTemplates: any[]): string { function getMermaidClasses(): string { let classStr = ""; for (const property in NODE_CONFIG) { - classStr += "classDef " + property + " fill:" + (NODE_CONFIG)[property].background + ",color:" + (NODE_CONFIG)[property].color + ",max-height:100px\n"; + classStr += "classDef " + property + " fill:" + (NODE_CONFIG)[property].background + ",color:" + (NODE_CONFIG)[property].color + ",text-decoration:none,max-height:100px\n"; } return classStr; } diff --git a/src/common/utils/mermaidUtils.ts b/src/common/utils/mermaidUtils.ts index f198e33de..2a72c2056 100644 --- a/src/common/utils/mermaidUtils.ts +++ b/src/common/utils/mermaidUtils.ts @@ -116,9 +116,9 @@ export async function generateMarkdownFileWithMermaidCli(outputFlowMdFileIn: str } export function getMermaidExtraClasses() { - const added = 'fill:green,color:white,stroke-width:4px,max-height:100px'; - const removed = 'fill:red,color:white,stroke-width:4px,max-height:100px'; - const changed = 'fill:orange,color:white,stroke-width:4px,max-height:100px'; + 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',