From df942bebb14c3bec570d69f27e282f8d3a794426 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Fri, 8 Aug 2025 17:17:59 +0200 Subject: [PATCH 01/12] Draft the js-like formatter --- src/commands/ftc/generate/code.ts | 6 +- src/format/JsFormatter.ts | 105 ++++++++++++++++++++++++++++++ src/parse/FlowParser.ts | 8 +-- 3 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/format/JsFormatter.ts diff --git a/src/commands/ftc/generate/code.ts b/src/commands/ftc/generate/code.ts index 59f1359..bff3d09 100644 --- a/src/commands/ftc/generate/code.ts +++ b/src/commands/ftc/generate/code.ts @@ -4,7 +4,8 @@ import { Messages } from '@salesforce/core'; import * as xml2js from 'xml2js'; import { Flow } from '../../../flow/Flow.js'; import { FlowParser, ParseTreeNode } from '../../../parse/FlowParser.js'; -import { DefaultFtcFormatter } from '../../../format/DefaultFtcFormatter.js'; +// import { DefaultFtcFormatter } from '../../../format/DefaultFtcFormatter.js'; +import { JsFormatter } from '../../../format/JsFormatter.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('flowtocode', 'ftc.generate.code'); @@ -54,9 +55,10 @@ export default class FtcGenerateCode extends SfCommand { const parser: xml2js.Parser = new xml2js.Parser({ explicitArray: false }); const flow: Flow = ((await parser.parseStringPromise(fileContent)) as ParsedXml).Flow; const flowParser: FlowParser = new FlowParser(); - const formatter: FormatterInterface = new DefaultFtcFormatter(); + const formatter: FormatterInterface = new JsFormatter(); // TODO: add format selection const treeNode: ParseTreeNode = flowParser.parse(flow); const parseTree: string = formatter.convertToPseudocode(treeNode); + this.log(parseTree); // TODO: remove me const outputPath: string = FtcGenerateCode.getOutputPath(filepath, flags.output); await fs.writeFile(outputPath, parseTree, 'utf-8'); this.log(`Output written to ${outputPath}`); diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts new file mode 100644 index 0000000..fbdaf60 --- /dev/null +++ b/src/format/JsFormatter.ts @@ -0,0 +1,105 @@ +import { ParseTreeNode, NodeType } from '../parse/FlowParser.js'; +import * as Flow from '../flow/Flow.js'; +import { FormatterInterface } from '../commands/ftc/generate/code.js'; + +export class JsFormatter implements FormatterInterface { + public convertToPseudocode(node: ParseTreeNode): string { + let result = ''; + result += 'main() {\n'; + result += this.formatNodeChildren(node); + result += '\n}'; + return result; + } + + private formatNodeChildren(node: ParseTreeNode): string { + return node.getChildren().map(child => this.formatNode(child)).join('\n'); + } + + private formatNode(node: ParseTreeNode): string { + const flowElement = node.getFlowElement() as Flow.FlowElement; + + switch (node.getType()) { + case NodeType.TRY: + return this.formatTryStatement(node); + case NodeType.DEFAULT_OUTCOME: + return this.formatDefaultOutcome(node); + case NodeType.EXCEPT: + return this.formatExceptStatement(node); + case NodeType.ACTION_CALL: + return this.formatActionCall(node); + case NodeType.DECISION: + return this.formatDecision(node); + case NodeType.SUBFLOW: + return this.formatSubflow(node); + case NodeType.CASE: + return this.formatRule(node); + case NodeType.ASSIGNMENT: + return this.formatAssignment(node); + case NodeType.SCREEN: + return this.formatFlowScreen(node); + case NodeType.LOOP: + return this.formatLoop(node); + case NodeType.ALREADY_VISITED: + return this.formatAlreadyVisited(flowElement); + default: + return flowElement.name; + } + } + + private formatAlreadyVisited(element: Flow.FlowElement): string { + return `GO TO ${element.name}`; // TODO: should be a proper function call + } + + private formatLoop(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowLoop; + return `foreach (${element.collectionReference} /*${element.iterationOrder}*/) + { + ${this.formatNodeChildren(node)} + } + `; + } + + private formatExceptStatement(node: ParseTreeNode): string { + return 'catch {\n' + this.formatNodeChildren(node) + '\n}'; + } + + private formatTryStatement(node: ParseTreeNode): string { + return 'try {\n' + this.formatNodeChildren(node) + '\n}'; + } + + private formatFlowScreen(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowScreen; + return `show_${element.name}(); // ${element.label}\n${this.formatNodeChildren(node)}`; + } + + private formatDefaultOutcome(node: ParseTreeNode): string { + return 'else {\n' + this.formatNodeChildren(node) + '\n}'; + } + + private formatAssignment(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowAssignment; + return `ASSIGNMENT: ${element.name};${this.formatNodeChildren(node)}`; + } + + private formatSubflow(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowSubflow; + return `call_${element.name}(); // ${element.flowName}\n${this.formatNodeChildren(node)}`; + } + + private formatDecision(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowDecision; + return `// ${element.label}. ${element.description ?? ''}\n${this.formatNodeChildren(node)}`; + } + + private formatRule(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowRule; + return `(?else)if (${element.conditionLogic} : ${JSON.stringify(element.conditions)}) { // ${element.label} ${element.description ?? ''} + ${this.formatNodeChildren(node)} + }`; + } + + private formatActionCall(node: ParseTreeNode): string { + const element = node.getFlowElement() as Flow.FlowActionCall; + return `do_${element.name}(); // ${element.label}\n${this.formatNodeChildren(node)}`; + } +} \ No newline at end of file diff --git a/src/parse/FlowParser.ts b/src/parse/FlowParser.ts index 9debec4..ee5d85b 100644 --- a/src/parse/FlowParser.ts +++ b/src/parse/FlowParser.ts @@ -20,10 +20,10 @@ export enum NodeType { export class ParseTreeNode { private type: NodeType; private parent?: ParseTreeNode; - private flowElement?: Flow.FlowBaseElement; + private flowElement?: Flow.FlowElement; private children: ParseTreeNode[]; - public constructor(type: NodeType, flowElement?: Flow.FlowBaseElement) { + public constructor(type: NodeType, flowElement?: Flow.FlowElement) { this.type = type; this.flowElement = flowElement; this.children = []; @@ -45,11 +45,11 @@ export class ParseTreeNode { this.parent = parent; } - public getFlowElement(): Flow.FlowBaseElement | undefined { + public getFlowElement(): Flow.FlowElement | undefined { return this.flowElement; } - public setFlowElement(flowElement: Flow.FlowBaseElement | undefined): void { + public setFlowElement(flowElement: Flow.FlowElement | undefined): void { this.flowElement = flowElement; } From ad628fe7b33abd6032b18fb1c9ca311f5017975a Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Fri, 8 Aug 2025 21:40:54 +0200 Subject: [PATCH 02/12] Further iteration of js generation --- package.json | 3 +- src/commands/ftc/generate/code.ts | 4 +- src/format/DefaultFtcFormatter.ts | 10 ++++- src/format/JsFormatter.ts | 61 +++++++++++++++++++++------- test/resources/test.flow.expected.js | 26 ++++++++++++ yarn.lock | 5 +++ 6 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 test/resources/test.flow.expected.js diff --git a/package.json b/package.json index 3c762f9..042c611 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@salesforce/sf-plugins-core": "^9.1.1", "@types/xml2js": "^0.4.14", "lodash": "4.17.21", + "prettier": "^3.6.2", "wireit": "^0.14.12", "xml2js": "^0.6.2" }, @@ -197,4 +198,4 @@ "lib": "lib", "test": "test" } -} +} \ No newline at end of file diff --git a/src/commands/ftc/generate/code.ts b/src/commands/ftc/generate/code.ts index bff3d09..4cae482 100644 --- a/src/commands/ftc/generate/code.ts +++ b/src/commands/ftc/generate/code.ts @@ -19,7 +19,7 @@ type ParsedXml = { }; export interface FormatterInterface { - convertToPseudocode(node: ParseTreeNode, tabLevel?: number): string; + convertToPseudocode(node: ParseTreeNode, tabLevel?: number): Promise; } export default class FtcGenerateCode extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -57,7 +57,7 @@ export default class FtcGenerateCode extends SfCommand { const flowParser: FlowParser = new FlowParser(); const formatter: FormatterInterface = new JsFormatter(); // TODO: add format selection const treeNode: ParseTreeNode = flowParser.parse(flow); - const parseTree: string = formatter.convertToPseudocode(treeNode); + const parseTree: string = await formatter.convertToPseudocode(treeNode); this.log(parseTree); // TODO: remove me const outputPath: string = FtcGenerateCode.getOutputPath(filepath, flags.output); await fs.writeFile(outputPath, parseTree, 'utf-8'); diff --git a/src/format/DefaultFtcFormatter.ts b/src/format/DefaultFtcFormatter.ts index e24e524..9db003a 100644 --- a/src/format/DefaultFtcFormatter.ts +++ b/src/format/DefaultFtcFormatter.ts @@ -3,13 +3,19 @@ import * as Flow from '../flow/Flow.js'; import { FormatterInterface } from '../commands/ftc/generate/code.js'; export class DefaultFtcFormatter implements FormatterInterface { - public convertToPseudocode(node: ParseTreeNode, tabLevel: number = -1): string { + public convertToPseudocode(node: ParseTreeNode): Promise { + return Promise.resolve( + this.formatPseudocode(node, -1) + ); + } + + public formatPseudocode(node: ParseTreeNode, tabLevel: number = -1): string { let result = ''; if (node.getType() !== NodeType.ROOT) { result += `${' '.repeat(tabLevel)}${this.formatNodeStatement(node)}\n`; } for (const child of node.getChildren()) { - result += this.convertToPseudocode(child, tabLevel + 1); + result += this.formatPseudocode(child, tabLevel + 1); } return result; } diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index fbdaf60..b42a0ac 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -1,14 +1,22 @@ +import prettier from 'prettier'; import { ParseTreeNode, NodeType } from '../parse/FlowParser.js'; import * as Flow from '../flow/Flow.js'; import { FormatterInterface } from '../commands/ftc/generate/code.js'; +import { FlowAssignmentItem } from '../flow/Flow.js'; export class JsFormatter implements FormatterInterface { - public convertToPseudocode(node: ParseTreeNode): string { + private functions: Map = new Map(); + + public convertToPseudocode(node: ParseTreeNode): Promise { let result = ''; - result += 'main() {\n'; + result += 'function main() {\n'; result += this.formatNodeChildren(node); result += '\n}'; - return result; + + const functions = Array.from(this.functions.entries()) + .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); + + return prettier.format(functions + '\n\n' + result, {parser: 'babel'}); } private formatNodeChildren(node: ParseTreeNode): string { @@ -42,12 +50,12 @@ export class JsFormatter implements FormatterInterface { case NodeType.ALREADY_VISITED: return this.formatAlreadyVisited(flowElement); default: - return flowElement.name; + return `${flowElement.name}();`; } } private formatAlreadyVisited(element: Flow.FlowElement): string { - return `GO TO ${element.name}`; // TODO: should be a proper function call + return `${element.name}();`; // TODO: should be a proper function call } private formatLoop(node: ParseTreeNode): string { @@ -60,46 +68,71 @@ export class JsFormatter implements FormatterInterface { } private formatExceptStatement(node: ParseTreeNode): string { - return 'catch {\n' + this.formatNodeChildren(node) + '\n}'; + return `catch (e) {\n${this.formatNodeChildren(node)}\n}`; } private formatTryStatement(node: ParseTreeNode): string { - return 'try {\n' + this.formatNodeChildren(node) + '\n}'; + return `try {\n${this.formatNodeChildren(node)}\n}`; } private formatFlowScreen(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowScreen; - return `show_${element.name}(); // ${element.label}\n${this.formatNodeChildren(node)}`; + // this.functions.set(element.name, `// Show ${element.label} ${element.description ?? ''}`); + return `${element.name}(); // Show ${element.label}${this.formatNodeChildren(node)}`; } private formatDefaultOutcome(node: ParseTreeNode): string { - return 'else {\n' + this.formatNodeChildren(node) + '\n}'; + return `else {${this.formatNodeChildren(node)}}`; } private formatAssignment(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowAssignment; - return `ASSIGNMENT: ${element.name};${this.formatNodeChildren(node)}`; + this.functions.set(element.name, this.formatAssignments(element.assignmentItems ?? [])); + return `${element.name}();${this.formatNodeChildren(node)}`; + } + + private formatAssignments(assignmentItems: FlowAssignmentItem[]): string { + return Array.isArray(assignmentItems) + ? assignmentItems.map((item: FlowAssignmentItem) => + `${item.assignToReference}${this.formatAssignOperator(item.operator)}${JSON.stringify(item.value)};` + ).join('') + : ''; } + private formatAssignOperator(operator: string): string { + switch (operator) { + case 'Add': + return ' += '; + case 'Subtract': + return ' -= '; + case 'AddItem': + return '[]= '; + case 'Assign': + return ' = '; + default: + return operator; + } + } + private formatSubflow(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowSubflow; - return `call_${element.name}(); // ${element.flowName}\n${this.formatNodeChildren(node)}`; + return `call_${element.name}(); // ${element.flowName}${this.formatNodeChildren(node)}`; } private formatDecision(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowDecision; - return `// ${element.label}. ${element.description ?? ''}\n${this.formatNodeChildren(node)}`; + return `// ${element.label}. ${element.description ?? ''}${this.formatNodeChildren(node)}`; } private formatRule(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowRule; - return `(?else)if (${element.conditionLogic} : ${JSON.stringify(element.conditions)}) { // ${element.label} ${element.description ?? ''} + return `/*?else*/ if (true/* ${element.conditionLogic} : ${JSON.stringify(element.conditions)} */) { // ${element.label} ${element.description ?? ''} ${this.formatNodeChildren(node)} }`; } private formatActionCall(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowActionCall; - return `do_${element.name}(); // ${element.label}\n${this.formatNodeChildren(node)}`; + return `do_${element.name}(); // ${element.label}${this.formatNodeChildren(node)}`; } } \ No newline at end of file diff --git a/test/resources/test.flow.expected.js b/test/resources/test.flow.expected.js new file mode 100644 index 0000000..a3a4cc5 --- /dev/null +++ b/test/resources/test.flow.expected.js @@ -0,0 +1,26 @@ +function Create_Request() { + ApexInvocableActionRequest.aid = { elementReference: "Action_Loop.Id" }; + ApexInvocableActionRequest.value = { elementReference: "Dollar_Amount" }; +} +function Add_Request() { + // TODO: why nothing here? +} + +function main() { + do_Execute_Apex_Query(); // Execute_Apex_Query + Select_Items_Screen(); // Show Select Items Screen + foreach(ADataTable.selectedRows /*Asc*/); + { + Create_Request(); + Add_Request(); + } + + try { + do_DoBulkAction(); // DoBulkAction + Confirmation_Screen(); // Show Confirmation Screen + Final_Screen(); // Show Final Screen + } catch (e) { + Fault_Screen(); // Show Fault Screen + Final_Screen(); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 73cff1b..cb196b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6606,6 +6606,11 @@ prettier@^2.8.8: resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + pretty-quick@^3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.3.1.tgz" From 0085a7dcd898ed7f2e551a17a9593d785fe0d418 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Fri, 8 Aug 2025 21:46:26 +0200 Subject: [PATCH 03/12] Fix missing line break --- src/format/JsFormatter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index b42a0ac..4e09cfc 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -82,7 +82,7 @@ export class JsFormatter implements FormatterInterface { } private formatDefaultOutcome(node: ParseTreeNode): string { - return `else {${this.formatNodeChildren(node)}}`; + return `else {\n${this.formatNodeChildren(node)}\n}`; } private formatAssignment(node: ParseTreeNode): string { @@ -121,12 +121,12 @@ export class JsFormatter implements FormatterInterface { private formatDecision(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowDecision; - return `// ${element.label}. ${element.description ?? ''}${this.formatNodeChildren(node)}`; + return `// ${element.label}. ${element.description ?? ''}\n${this.formatNodeChildren(node)}`; } private formatRule(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowRule; - return `/*?else*/ if (true/* ${element.conditionLogic} : ${JSON.stringify(element.conditions)} */) { // ${element.label} ${element.description ?? ''} + return `/* ?else */ if (true /* ${element.conditionLogic} : ${JSON.stringify(element.conditions)} */) { // ${element.label} ${element.description ?? ''} ${this.formatNodeChildren(node)} }`; } From f38ac3ad0a8efa546a6db1963200a57ebd298b9e Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Mon, 11 Aug 2025 16:13:51 +0200 Subject: [PATCH 04/12] Implement visited connectors, variables, and more --- src/commands/ftc/generate/code.ts | 4 +- src/flow/Flow.ts | 22 ++++++ src/format/JsFormatter.ts | 113 +++++++++++++++++++++++------- src/parse/FlowParser.ts | 10 ++- 4 files changed, 121 insertions(+), 28 deletions(-) diff --git a/src/commands/ftc/generate/code.ts b/src/commands/ftc/generate/code.ts index 4cae482..ac4f97e 100644 --- a/src/commands/ftc/generate/code.ts +++ b/src/commands/ftc/generate/code.ts @@ -52,8 +52,8 @@ export default class FtcGenerateCode extends SfCommand { const filepath: string = flags.file; const fileContent: string = await fs.readFile(filepath, 'utf-8'); - const parser: xml2js.Parser = new xml2js.Parser({ explicitArray: false }); - const flow: Flow = ((await parser.parseStringPromise(fileContent)) as ParsedXml).Flow; + const xmlParser: xml2js.Parser = new xml2js.Parser({ explicitArray: false, valueProcessors: [xml2js.processors.parseBooleans] }); + const flow: Flow = ((await xmlParser.parseStringPromise(fileContent)) as ParsedXml).Flow; const flowParser: FlowParser = new FlowParser(); const formatter: FormatterInterface = new JsFormatter(); // TODO: add format selection const treeNode: ParseTreeNode = flowParser.parse(flow); diff --git a/src/flow/Flow.ts b/src/flow/Flow.ts index b52ab8d..20b3916 100644 --- a/src/flow/Flow.ts +++ b/src/flow/Flow.ts @@ -37,6 +37,7 @@ export type Flow = Metadata & { timeZoneSidKey?: string; transforms?: FlowTransform[]; triggerOrder?: number; + variables?: FlowVariable[]; }; export function isFlow(obj: unknown): obj is Flow { return typeof obj === 'object' && obj !== null && 'description' in obj; @@ -108,11 +109,17 @@ export type FlowActionCall = FlowNode & { actionType: string; connector: FlowConnector; faultConnector: FlowConnector; + inputParameters: FlowActionCallInputParameter[]; }; export function isFlowActionCall(obj: unknown): obj is FlowActionCall { return typeof obj === 'object' && obj !== null && 'actionName' in obj; } +export type FlowActionCallInputParameter = { + name: string; + value: FlowElementReferenceOrValue; +}; + export type FlowLoop = FlowNode & { assignNextValueToReference?: string; collectionReference: string; @@ -233,6 +240,17 @@ export type FlowTransformValue = { rightValue: FlowElementReferenceOrValue; }; +export type FlowVariable = FlowElement & { + apexClass: string; + dataType: FlowDataType; + isCollection: boolean; + isInput: boolean; + isOutput: boolean; + objectType: string; + scale: number; + value: FlowElementReferenceOrValue; +}; + // ===================================================================================================================== // Flow Enums // ===================================================================================================================== @@ -253,12 +271,16 @@ export enum FlowAssignmentOperator { } export enum FlowDataType { + Apex = 'Apex', Boolean = 'Boolean', Currency = 'Currency', Date = 'Date', DateTime = 'DateTime', Number = 'Number', + Multipicklist = 'Multipicklist', + Picklist = 'Picklist', String = 'String', + sObject = 'sObject', } export enum FlowWaitConditionType { diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index 4e09cfc..91bb962 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -1,28 +1,62 @@ import prettier from 'prettier'; -import { ParseTreeNode, NodeType } from '../parse/FlowParser.js'; +import { ParseTreeNode, NodeType, RootNode } from '../parse/FlowParser.js'; import * as Flow from '../flow/Flow.js'; import { FormatterInterface } from '../commands/ftc/generate/code.js'; import { FlowAssignmentItem } from '../flow/Flow.js'; export class JsFormatter implements FormatterInterface { private functions: Map = new Map(); + private revisitedElements: string[] = []; public convertToPseudocode(node: ParseTreeNode): Promise { - let result = ''; - result += 'function main() {\n'; - result += this.formatNodeChildren(node); - result += '\n}'; + + let result = ''; + let variables: Flow.FlowVariable[] = []; + + this.revisitedElements = this.filterNodes(node, n => n.getType() === NodeType.ALREADY_VISITED) + .map(n => n.getFlowElement()?.name ?? ''); - const functions = Array.from(this.functions.entries()) - .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); + if (node.getType() === NodeType.ROOT) { + variables = (node as RootNode).flow.variables ?? []; + } - return prettier.format(functions + '\n\n' + result, {parser: 'babel'}); + result += 'function main(' + + variables.filter(v => v.isInput).map(v => `${v.name}: ${v.dataType}`).join(', ') + + ') {\n' + + variables.filter(v => !v.isInput).map(v => ` let ${v.name}: ${v.dataType} = ${v.value ? this.formatFlowElementReferenceOrValue(v.value) : 'null'};`).join('\n') + + '\n\n' + + this.formatNodeChildren(node) + + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + + '\n}'; + + // Functions may be only collected after the formating round, as they are "pitched" from the traverse + const functions = Array.from(this.functions.entries()) + .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); + + return prettier.format(functions + '\n\n' + result, {parser: 'babel-ts'}); } private formatNodeChildren(node: ParseTreeNode): string { return node.getChildren().map(child => this.formatNode(child)).join('\n'); } + private filterNodes(node: ParseTreeNode, callback: (node: ParseTreeNode) => boolean): ParseTreeNode[] { + const results: ParseTreeNode[] = []; + + const traverse = (currentNode: ParseTreeNode): void => { + if (callback(currentNode)) { + results.push(currentNode); + } + + for (const child of currentNode.getChildren()) { + traverse(child); + } + }; + + traverse(node); + return results; + } + private formatNode(node: ParseTreeNode): string { const flowElement = node.getFlowElement() as Flow.FlowElement; @@ -42,7 +76,7 @@ export class JsFormatter implements FormatterInterface { case NodeType.CASE: return this.formatRule(node); case NodeType.ASSIGNMENT: - return this.formatAssignment(node); + return this.formatAssignmentNode(node); case NodeType.SCREEN: return this.formatFlowScreen(node); case NodeType.LOOP: @@ -54,17 +88,36 @@ export class JsFormatter implements FormatterInterface { } } + private formatNodeChain(node: ParseTreeNode, nodeOwnBody: string): string { + if (node.getFlowElement() === null || node.getFlowElement() === undefined) { + return nodeOwnBody; + } + + const element = node.getFlowElement() as Flow.FlowElement; + + // Revisited nodes should be encapsulated as functions to call them from multiple places of the tree + if (this.revisitedElements.includes(element.name)) { + this.functions.set(element.name, nodeOwnBody + this.formatNodeChildren(node)); + return `${element.name}()\n`; + } + + return nodeOwnBody; + } + private formatAlreadyVisited(element: Flow.FlowElement): string { - return `${element.name}();`; // TODO: should be a proper function call + return `${element.name}();`; } private formatLoop(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowLoop; - return `foreach (${element.collectionReference} /*${element.iterationOrder}*/) + const body = ` + // ${element.label} + for (let ${element.name} of ${element.collectionReference} /*${element.iterationOrder}*/) { ${this.formatNodeChildren(node)} } `; + return this.formatNodeChain(node, body); } private formatExceptStatement(node: ParseTreeNode): string { @@ -77,26 +130,32 @@ export class JsFormatter implements FormatterInterface { private formatFlowScreen(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowScreen; - // this.functions.set(element.name, `// Show ${element.label} ${element.description ?? ''}`); - return `${element.name}(); // Show ${element.label}${this.formatNodeChildren(node)}`; + return this.formatNodeChain(node, `${element.name}.show(); // Show ${element.label} ${element.description ?? ''}`); } private formatDefaultOutcome(node: ParseTreeNode): string { return `else {\n${this.formatNodeChildren(node)}\n}`; } - private formatAssignment(node: ParseTreeNode): string { + private formatAssignmentNode(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowAssignment; - this.functions.set(element.name, this.formatAssignments(element.assignmentItems ?? [])); - return `${element.name}();${this.formatNodeChildren(node)}`; + return this.formatNodeChain(node, `// ${element.label}\n${this.formatAssignments(element.assignmentItems ?? [])}`); } private formatAssignments(assignmentItems: FlowAssignmentItem[]): string { - return Array.isArray(assignmentItems) - ? assignmentItems.map((item: FlowAssignmentItem) => - `${item.assignToReference}${this.formatAssignOperator(item.operator)}${JSON.stringify(item.value)};` - ).join('') - : ''; + const items = Array.isArray(assignmentItems) ? assignmentItems : [assignmentItems]; + + return items.map((item: FlowAssignmentItem) => + `${item.assignToReference}${this.formatAssignOperator(item.operator)}${this.formatFlowElementReferenceOrValue(item.value)};` + ).join(''); + } + + private formatFlowElementReferenceOrValue(value: Flow.FlowElementReferenceOrValue): string { + if (value.elementReference !== undefined) { + return value.elementReference; + } else { + return JSON.stringify(value); + } } private formatAssignOperator(operator: string): string { @@ -112,16 +171,16 @@ export class JsFormatter implements FormatterInterface { default: return operator; } - } + } private formatSubflow(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowSubflow; - return `call_${element.name}(); // ${element.flowName}${this.formatNodeChildren(node)}`; + return this.formatNodeChain(node, `// Call subflow ${element.flowName}$`); } private formatDecision(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowDecision; - return `// ${element.label}. ${element.description ?? ''}\n${this.formatNodeChildren(node)}`; + return this.formatNodeChain(node, `// ${element.label}. ${element.description ?? ''}`); } private formatRule(node: ParseTreeNode): string { @@ -133,6 +192,10 @@ export class JsFormatter implements FormatterInterface { private formatActionCall(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowActionCall; - return `do_${element.name}(); // ${element.label}${this.formatNodeChildren(node)}`; + const params = Array.isArray(element.inputParameters) ? element.inputParameters : [element.inputParameters]; + const body = `${element.actionType}.${element.actionName}({ + ${params.map(param => param.name + ': ' + this.formatFlowElementReferenceOrValue(param.value)).join(', ')} + });`; + return this.formatNodeChain(node, body); } } \ No newline at end of file diff --git a/src/parse/FlowParser.ts b/src/parse/FlowParser.ts index ee5d85b..2149a91 100644 --- a/src/parse/FlowParser.ts +++ b/src/parse/FlowParser.ts @@ -63,6 +63,14 @@ export class ParseTreeNode { } } +export class RootNode extends ParseTreeNode { + public flow: Flow.Flow; + public constructor(flowElement: Flow.Flow) { + super(NodeType.ROOT); + this.flow = flowElement; + } +} + export class FlowParser { private flowElementByName: Map; private flowLoopStack: Flow.FlowLoop[]; @@ -77,7 +85,7 @@ export class FlowParser { public parse(flow: Flow.Flow): ParseTreeNode { this.flowElementByName = this.getFlowElementByName(flow); - const root = new ParseTreeNode(NodeType.ROOT); + const root = new RootNode(flow); if (flow.start.connector) { const startElement: Flow.FlowElement = this.getElementFromConnector(flow.start.connector); From ddd71c7ca2f650359a46bf92744d67b7c78c5dc7 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Mon, 11 Aug 2025 16:26:40 +0200 Subject: [PATCH 05/12] Format mein function --- src/format/JsFormatter.ts | 30 +++++++++++------ test/resources/test.flow.expected.js | 48 +++++++++++++++++----------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index 91bb962..a59170e 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -9,31 +9,41 @@ export class JsFormatter implements FormatterInterface { private revisitedElements: string[] = []; public convertToPseudocode(node: ParseTreeNode): Promise { - - let result = ''; let variables: Flow.FlowVariable[] = []; + let flowName = 'main'; + let description = `/** ${node.getFlowElement()?.description ?? ''} **/`; this.revisitedElements = this.filterNodes(node, n => n.getType() === NodeType.ALREADY_VISITED) .map(n => n.getFlowElement()?.name ?? ''); if (node.getType() === NodeType.ROOT) { - variables = (node as RootNode).flow.variables ?? []; + const flow = (node as RootNode).flow; + variables = flow.variables ?? []; + flowName = flow.fullName ?? 'main'; + description = `/** + ${flow.label ? flow.label : ''} + ${flow.description ?? ''} **/`; } - result += 'function main(' + const childrenCode = this.formatNodeChildren(node); + // Functions may be only collected after the formating round, as they are "pitched" from the traverse + const functions = Array.from(this.functions.entries()) + .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); + + const result = ` + ${description} + function ${flowName}(` + variables.filter(v => v.isInput).map(v => `${v.name}: ${v.dataType}`).join(', ') + ') {\n' + variables.filter(v => !v.isInput).map(v => ` let ${v.name}: ${v.dataType} = ${v.value ? this.formatFlowElementReferenceOrValue(v.value) : 'null'};`).join('\n') + '\n\n' - + this.formatNodeChildren(node) + + functions + + '\n\n' + + childrenCode + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + '\n}'; - - // Functions may be only collected after the formating round, as they are "pitched" from the traverse - const functions = Array.from(this.functions.entries()) - .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); - return prettier.format(functions + '\n\n' + result, {parser: 'babel-ts'}); + return prettier.format(result, {parser: 'babel-ts'}); } private formatNodeChildren(node: ParseTreeNode): string { diff --git a/test/resources/test.flow.expected.js b/test/resources/test.flow.expected.js index a3a4cc5..e0a6f33 100644 --- a/test/resources/test.flow.expected.js +++ b/test/resources/test.flow.expected.js @@ -1,26 +1,38 @@ -function Create_Request() { - ApexInvocableActionRequest.aid = { elementReference: "Action_Loop.Id" }; - ApexInvocableActionRequest.value = { elementReference: "Dollar_Amount" }; -} -function Add_Request() { - // TODO: why nothing here? -} +/** + Test Flow + Test flow. **/ +function main(RecordId: String) { + let ItemsToProcess: SObject = null; + let ApexInvocableActionRequest: Apex = null; + let ActionRequests: Apex = null; -function main() { - do_Execute_Apex_Query(); // Execute_Apex_Query - Select_Items_Screen(); // Show Select Items Screen - foreach(ADataTable.selectedRows /*Asc*/); - { - Create_Request(); - Add_Request(); + function Final_Screen() { + Final_Screen.show(); // Show Final Screen + } + + apex.ApexInvocableQuery({ + accountIds: RecordId, + }); + Select_Items_Screen.show(); // Show Select Items Screen + + // Action Loop + for (let Action_Loop of ADataTable.selectedRows /*Asc*/) { + // Create Request + ApexInvocableActionRequest.aid = Action_Loop.Id; + ApexInvocableActionRequest.value = Dollar_Amount; + // Add Request + ActionRequests += ApexInvocableActionRequest; } try { - do_DoBulkAction(); // DoBulkAction - Confirmation_Screen(); // Show Confirmation Screen - Final_Screen(); // Show Final Screen + apex.ApexInvocableAction({ + ApexInvocableActionRequests: ActionRequests, + }); + Confirmation_Screen.show(); // Show Confirmation Screen + Final_Screen(); } catch (e) { - Fault_Screen(); // Show Fault Screen + Fault_Screen.show(); // Show Fault Screen Final_Screen(); } + return []; } \ No newline at end of file From 3098ab4816365b1bd02db54da2523165a177772f Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Tue, 12 Aug 2025 12:12:40 +0200 Subject: [PATCH 06/12] Possible bracking change. Build node tree properly. --- src/parse/FlowParser.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/parse/FlowParser.ts b/src/parse/FlowParser.ts index 2149a91..6f25a80 100644 --- a/src/parse/FlowParser.ts +++ b/src/parse/FlowParser.ts @@ -130,14 +130,17 @@ export class FlowParser { } else if (Flow.isFlowDecision(element)) { this.parseDecisionElement(parentNode, element); } else if (Flow.isFlowAssignment(element)) { - parentNode.addChild(new ParseTreeNode(NodeType.ASSIGNMENT, element)); - this.parseConnector(parentNode, element); + const assignmentNode = new ParseTreeNode(NodeType.ASSIGNMENT, element); + parentNode.addChild(assignmentNode); + this.parseConnector(assignmentNode, element); } else if (Flow.isFlowSubflow(element)) { - parentNode.addChild(new ParseTreeNode(NodeType.SUBFLOW, element)); - this.parseConnector(parentNode, element); + const subflowNode = new ParseTreeNode(NodeType.SUBFLOW, element); + parentNode.addChild(subflowNode); + this.parseConnector(subflowNode, element); } else { - parentNode.addChild(new ParseTreeNode(NodeType.OTHER, element)); - this.parseConnector(parentNode, element); + const otherNode = new ParseTreeNode(NodeType.OTHER, element); + parentNode.addChild(otherNode); + this.parseConnector(otherNode, element); } } @@ -162,8 +165,9 @@ export class FlowParser { } private parseScreenElement(parentNode: ParseTreeNode, screenElement: Flow.FlowScreen): void { - parentNode.addChild(new ParseTreeNode(NodeType.SCREEN, screenElement)); - this.parseConnector(parentNode, screenElement); + const screenNode = new ParseTreeNode(NodeType.SCREEN, screenElement); + parentNode.addChild(screenNode); + this.parseConnector(screenNode, screenElement); } private parseLoopElement(parentNode: ParseTreeNode, flowElement: Flow.FlowLoop): void { From f9f2fd6f1d3f6f4b1b74f61048d10e4731e0f6f5 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Tue, 12 Aug 2025 12:16:25 +0200 Subject: [PATCH 07/12] Fix tree traversing --- src/format/JsFormatter.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index a59170e..c3dde06 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -42,7 +42,7 @@ export class JsFormatter implements FormatterInterface { + childrenCode + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + '\n}'; - + return prettier.format(result, {parser: 'babel-ts'}); } @@ -94,7 +94,7 @@ export class JsFormatter implements FormatterInterface { case NodeType.ALREADY_VISITED: return this.formatAlreadyVisited(flowElement); default: - return `${flowElement.name}();`; + return this.formatNodeChain(node, `// ${flowElement.name} ${JSON.stringify(flowElement)}`); } } @@ -107,11 +107,13 @@ export class JsFormatter implements FormatterInterface { // Revisited nodes should be encapsulated as functions to call them from multiple places of the tree if (this.revisitedElements.includes(element.name)) { - this.functions.set(element.name, nodeOwnBody + this.formatNodeChildren(node)); - return `${element.name}()\n`; + if (!this.functions.has(element.name)) { + this.functions.set(element.name, nodeOwnBody + this.formatNodeChildren(node)); + } + return `${element.name}();\n`; } - return nodeOwnBody; + return nodeOwnBody + '\n' + this.formatNodeChildren(node); } private formatAlreadyVisited(element: Flow.FlowElement): string { @@ -185,7 +187,7 @@ export class JsFormatter implements FormatterInterface { private formatSubflow(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowSubflow; - return this.formatNodeChain(node, `// Call subflow ${element.flowName}$`); + return this.formatNodeChain(node, `subflows.${element.flowName}.call();\n`); // TODO: render parameters } private formatDecision(node: ParseTreeNode): string { From 369ba953420397a5fcacb23781c5c34a9c89d77e Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Tue, 12 Aug 2025 14:03:41 +0200 Subject: [PATCH 08/12] Improve condition formatting --- src/format/JsFormatter.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index c3dde06..4885a34 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -47,7 +47,7 @@ export class JsFormatter implements FormatterInterface { } private formatNodeChildren(node: ParseTreeNode): string { - return node.getChildren().map(child => this.formatNode(child)).join('\n'); + return node.getChildren().map((child, index) => this.formatNode(child, index)).join('\n'); } private filterNodes(node: ParseTreeNode, callback: (node: ParseTreeNode) => boolean): ParseTreeNode[] { @@ -67,7 +67,7 @@ export class JsFormatter implements FormatterInterface { return results; } - private formatNode(node: ParseTreeNode): string { + private formatNode(node: ParseTreeNode, sequenceNr: number = 0): string { const flowElement = node.getFlowElement() as Flow.FlowElement; switch (node.getType()) { @@ -84,7 +84,7 @@ export class JsFormatter implements FormatterInterface { case NodeType.SUBFLOW: return this.formatSubflow(node); case NodeType.CASE: - return this.formatRule(node); + return this.formatRule(node, sequenceNr); case NodeType.ASSIGNMENT: return this.formatAssignmentNode(node); case NodeType.SCREEN: @@ -163,11 +163,7 @@ export class JsFormatter implements FormatterInterface { } private formatFlowElementReferenceOrValue(value: Flow.FlowElementReferenceOrValue): string { - if (value.elementReference !== undefined) { - return value.elementReference; - } else { - return JSON.stringify(value); - } + return value.elementReference ?? JSON.stringify(value); } private formatAssignOperator(operator: string): string { @@ -195,9 +191,23 @@ export class JsFormatter implements FormatterInterface { return this.formatNodeChain(node, `// ${element.label}. ${element.description ?? ''}`); } - private formatRule(node: ParseTreeNode): string { + private formatRule(node: ParseTreeNode, sequenceNr: number): string { const element = node.getFlowElement() as Flow.FlowRule; - return `/* ?else */ if (true /* ${element.conditionLogic} : ${JSON.stringify(element.conditions)} */) { // ${element.label} ${element.description ?? ''} + const conditions = Array.isArray(element.conditions) ? element.conditions : [element.conditions]; + + let condition: string = + element.conditionLogic === 'and' ? Array.from(conditions.keys()).map(key => key + 1).join(' AND ') : + element.conditionLogic === 'or' ? Array.from(conditions.keys()).map(key => key + 1).join(' OR ') : + element.conditionLogic; + + for (const [index, flowCondition] of conditions.entries()) { + condition = condition.replace( + String(index + 1), + `${flowCondition.operator}(${flowCondition.leftValueReference}, ${this.formatFlowElementReferenceOrValue(flowCondition.rightValue)})` + ); + } + + return `${sequenceNr ? 'else' : ''} if (${condition}) { // ${element.label} ${element.description ?? ''} ${this.formatNodeChildren(node)} }`; } From 0e52e832d3bd4f61e310b0b55a2635796a518a5b Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Tue, 12 Aug 2025 15:38:19 +0200 Subject: [PATCH 09/12] Improve condition and value formatting --- src/format/JsFormatter.ts | 46 +++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index 4885a34..31b39c3 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -43,7 +43,7 @@ export class JsFormatter implements FormatterInterface { + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + '\n}'; - return prettier.format(result, {parser: 'babel-ts'}); + return prettier.format(result, {parser: 'babel-ts', printWidth: 300}); } private formatNodeChildren(node: ParseTreeNode): string { @@ -94,7 +94,7 @@ export class JsFormatter implements FormatterInterface { case NodeType.ALREADY_VISITED: return this.formatAlreadyVisited(flowElement); default: - return this.formatNodeChain(node, `// ${flowElement.name} ${JSON.stringify(flowElement)}`); + return this.formatNodeChain(node, `${flowElement.name}(); // ${JSON.stringify(flowElement)}`); } } @@ -133,20 +133,20 @@ export class JsFormatter implements FormatterInterface { } private formatExceptStatement(node: ParseTreeNode): string { - return `catch (e) {\n${this.formatNodeChildren(node)}\n}`; + return `catch (e) {\n${this.formatNodeChildren(node)}\n}`; // TODO: should it call formatNodeChain? } private formatTryStatement(node: ParseTreeNode): string { - return `try {\n${this.formatNodeChildren(node)}\n}`; + return `try {\n${this.formatNodeChildren(node)}\n}`; // TODO: should it call formatNodeChain? } private formatFlowScreen(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowScreen; - return this.formatNodeChain(node, `${element.name}.show(); // Show ${element.label} ${element.description ?? ''}`); + return this.formatNodeChain(node, `screen.show('${element.name}'); // Show ${element.label} ${element.description ?? ''}\n`); } private formatDefaultOutcome(node: ParseTreeNode): string { - return `else {\n${this.formatNodeChildren(node)}\n}`; + return `else {\n${this.formatNodeChildren(node)}\n}`; // TODO: should it call formatNodeChain? } private formatAssignmentNode(node: ParseTreeNode): string { @@ -163,7 +163,17 @@ export class JsFormatter implements FormatterInterface { } private formatFlowElementReferenceOrValue(value: Flow.FlowElementReferenceOrValue): string { - return value.elementReference ?? JSON.stringify(value); + if (value.elementReference !== undefined) { + return value.elementReference; + } else if (value.numberValue !== undefined && value.numberValue !== null) { + return value.numberValue.toString(); + } else if (value.stringValue !== undefined && value.stringValue !== null) { + return `"${value.stringValue ?? ''}"`; + } else if (value.booleanValue !== undefined && value.booleanValue !== null) { + return value.booleanValue.toString(); + } else { + return JSON.stringify(value); + } } private formatAssignOperator(operator: string): string { @@ -183,12 +193,12 @@ export class JsFormatter implements FormatterInterface { private formatSubflow(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowSubflow; - return this.formatNodeChain(node, `subflows.${element.flowName}.call();\n`); // TODO: render parameters + return this.formatNodeChain(node, `subflow.call('${element.flowName}');\n`); // TODO: render parameters } private formatDecision(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowDecision; - return this.formatNodeChain(node, `// ${element.label}. ${element.description ?? ''}`); + return this.formatNodeChain(node, `// Check: ${element.label}. ${element.description ?? ''}`); } private formatRule(node: ParseTreeNode, sequenceNr: number): string { @@ -203,19 +213,31 @@ export class JsFormatter implements FormatterInterface { for (const [index, flowCondition] of conditions.entries()) { condition = condition.replace( String(index + 1), - `${flowCondition.operator}(${flowCondition.leftValueReference}, ${this.formatFlowElementReferenceOrValue(flowCondition.rightValue)})` + this.formatCondition(flowCondition) ); } - return `${sequenceNr ? 'else' : ''} if (${condition}) { // ${element.label} ${element.description ?? ''} + return `${sequenceNr ? 'else' : ''} if (${condition}) { // Case: ${element.label} ${element.description ?? ''} ${this.formatNodeChildren(node)} }`; } + private formatCondition(flowCondition: Flow.FlowCondition): string { + if (flowCondition.operator === Flow.FlowComparisonOperator.EqualTo) { + return `(${flowCondition.leftValueReference} === ${this.formatFlowElementReferenceOrValue(flowCondition.rightValue)})`; + } else if (flowCondition.operator === Flow.FlowComparisonOperator.NotEqualTo) { + return `(${flowCondition.leftValueReference} !== ${this.formatFlowElementReferenceOrValue(flowCondition.rightValue)})`; + } else if (flowCondition.operator === Flow.FlowComparisonOperator.IsNull && flowCondition.rightValue.booleanValue !== null) { + return `(${flowCondition.leftValueReference} ${flowCondition.rightValue.booleanValue ? '=' : '!'}== null)`; + } else { + return `${flowCondition.operator}(${flowCondition.leftValueReference}, ${this.formatFlowElementReferenceOrValue(flowCondition.rightValue)})`; + } + } + private formatActionCall(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowActionCall; const params = Array.isArray(element.inputParameters) ? element.inputParameters : [element.inputParameters]; - const body = `${element.actionType}.${element.actionName}({ + const body = `${element.actionType}.call('${element.actionName}', { ${params.map(param => param.name + ': ' + this.formatFlowElementReferenceOrValue(param.value)).join(', ')} });`; return this.formatNodeChain(node, body); From 8f393a7954aaebced9a980768aa1eff78d8774d7 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Tue, 12 Aug 2025 17:00:00 +0200 Subject: [PATCH 10/12] Fallback to linear tree and goto fake --- src/format/JsFormatter.ts | 17 ++++++++--------- src/parse/FlowParser.ts | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/format/JsFormatter.ts b/src/format/JsFormatter.ts index 31b39c3..c552d06 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/JsFormatter.ts @@ -27,8 +27,7 @@ export class JsFormatter implements FormatterInterface { const childrenCode = this.formatNodeChildren(node); // Functions may be only collected after the formating round, as they are "pitched" from the traverse - const functions = Array.from(this.functions.entries()) - .map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); + // const functions = Array.from(this.functions.entries()).map(([name, body]) => `function ${name}() {\n${body}\n}`).join(''); const result = ` ${description} @@ -37,12 +36,12 @@ export class JsFormatter implements FormatterInterface { + ') {\n' + variables.filter(v => !v.isInput).map(v => ` let ${v.name}: ${v.dataType} = ${v.value ? this.formatFlowElementReferenceOrValue(v.value) : 'null'};`).join('\n') + '\n\n' - + functions + // + functions + '\n\n' + childrenCode + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + '\n}'; - + // return Promise.resolve(result); return prettier.format(result, {parser: 'babel-ts', printWidth: 300}); } @@ -108,16 +107,16 @@ export class JsFormatter implements FormatterInterface { // Revisited nodes should be encapsulated as functions to call them from multiple places of the tree if (this.revisitedElements.includes(element.name)) { if (!this.functions.has(element.name)) { - this.functions.set(element.name, nodeOwnBody + this.formatNodeChildren(node)); + this.functions.set(element.name, nodeOwnBody); } - return `${element.name}();\n`; + return `// [lbl] ${element.name}:\n${nodeOwnBody} \n ${this.formatNodeChildren(node)}`; } return nodeOwnBody + '\n' + this.formatNodeChildren(node); } private formatAlreadyVisited(element: Flow.FlowElement): string { - return `${element.name}();`; + return `// goto ${element.name};`; } private formatLoop(node: ParseTreeNode): string { @@ -129,7 +128,7 @@ export class JsFormatter implements FormatterInterface { ${this.formatNodeChildren(node)} } `; - return this.formatNodeChain(node, body); + return body; } private formatExceptStatement(node: ParseTreeNode): string { @@ -239,7 +238,7 @@ export class JsFormatter implements FormatterInterface { const params = Array.isArray(element.inputParameters) ? element.inputParameters : [element.inputParameters]; const body = `${element.actionType}.call('${element.actionName}', { ${params.map(param => param.name + ': ' + this.formatFlowElementReferenceOrValue(param.value)).join(', ')} - });`; + }); // ${element.label} ${element.description ?? ''}`; return this.formatNodeChain(node, body); } } \ No newline at end of file diff --git a/src/parse/FlowParser.ts b/src/parse/FlowParser.ts index 6f25a80..6547f71 100644 --- a/src/parse/FlowParser.ts +++ b/src/parse/FlowParser.ts @@ -132,15 +132,15 @@ export class FlowParser { } else if (Flow.isFlowAssignment(element)) { const assignmentNode = new ParseTreeNode(NodeType.ASSIGNMENT, element); parentNode.addChild(assignmentNode); - this.parseConnector(assignmentNode, element); + this.parseConnector(parentNode, element); } else if (Flow.isFlowSubflow(element)) { const subflowNode = new ParseTreeNode(NodeType.SUBFLOW, element); parentNode.addChild(subflowNode); - this.parseConnector(subflowNode, element); + this.parseConnector(parentNode, element); } else { const otherNode = new ParseTreeNode(NodeType.OTHER, element); parentNode.addChild(otherNode); - this.parseConnector(otherNode, element); + this.parseConnector(parentNode, element); } } @@ -167,7 +167,7 @@ export class FlowParser { private parseScreenElement(parentNode: ParseTreeNode, screenElement: Flow.FlowScreen): void { const screenNode = new ParseTreeNode(NodeType.SCREEN, screenElement); parentNode.addChild(screenNode); - this.parseConnector(screenNode, screenElement); + this.parseConnector(parentNode, screenElement); } private parseLoopElement(parentNode: ParseTreeNode, flowElement: Flow.FlowLoop): void { From 0b696cdc3aa7f69ff88bae81124f23789e34ab53 Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Wed, 13 Aug 2025 09:28:07 +0200 Subject: [PATCH 11/12] Kinda expected js with goto --- test/resources/test.flow.expected.js | Bin 991 -> 2130 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/resources/test.flow.expected.js b/test/resources/test.flow.expected.js index e0a6f33c70545a86fdceed87c4f7af87668ca4cd..7a7bfe8ab65564ed14462bc0b2024b25263d85b4 100644 GIT binary patch literal 2130 zcmbW2PjAye5XI+=#CNcA3Z-)HtvHmHNR^O)Hn$2{CUr`{b-+m*D%7tI{AM@n-Hp2? zimZ)l@BDf5-pusxpOGB!Wb#s8@avz6RI-#6U&d0)Le|e%%r+J|)*5YOY-e&OjZBF# z$G_q3R!aVEWGELzpYpYpYdPl}aeIknYwmC474N^hj5XGZSq^&$)bqH3^p(Ef5T93(JM>FzTRbZAyrg)}NZueff4fCC!PSYRwT(wrcYhHEQ`Db-mW^>Q}GyM4u9&VJ_yd zqrJnVos_ z=DnB3F-#}me=BSqER@-f3KbO+h~soRsX8rPZ8S8Z*3lBBv1MLfK;dkypJwpMg9;tw z4x4sm?yQm6w$yODc|qym5_G53{MeSg;zzFEjT9S&nTKBDTSqi5fZu-f4_gR!+{36n z!$qxyS{KqH>gd1>U|3DsXS0oFb4XJt=x=z0Fv3{->4ZOmh$JJX_Z`|@Mz-iWtKs31_Uyj^>wPr~ From 7ebfc995397dee29f67d6de096d54423c234ec9b Mon Sep 17 00:00:00 2001 From: Stepan Stepanov Date: Sun, 12 Oct 2025 21:05:49 +0200 Subject: [PATCH 12/12] Add format switcher and smaller improvements --- messages/ftc.generate.code.md | 4 + src/commands/ftc/generate/code.ts | 30 +++++-- src/format/{JsFormatter.ts => TsFormatter.ts} | 76 +++++++++--------- test/resources/test.flow.expected.js | Bin 2130 -> 0 bytes test/resources/test.flow.expected.typescript | 40 +++++++++ 5 files changed, 104 insertions(+), 46 deletions(-) rename src/format/{JsFormatter.ts => TsFormatter.ts} (81%) delete mode 100644 test/resources/test.flow.expected.js create mode 100644 test/resources/test.flow.expected.typescript diff --git a/messages/ftc.generate.code.md b/messages/ftc.generate.code.md index 0778987..156f7c8 100644 --- a/messages/ftc.generate.code.md +++ b/messages/ftc.generate.code.md @@ -14,6 +14,10 @@ The flow file to convert to pseudocode. The output directory for the pseudocode. If not provided, the pseudocode will be written to the same directory as the flow file. +# flags.output-format.summary + +Output file format. Default 'ftc' or 'ts' for typescript-similar file. + # examples - <%= config.bin %> <%= command.id %> -f ./test.flow diff --git a/src/commands/ftc/generate/code.ts b/src/commands/ftc/generate/code.ts index ac4f97e..16d7c67 100644 --- a/src/commands/ftc/generate/code.ts +++ b/src/commands/ftc/generate/code.ts @@ -4,8 +4,8 @@ import { Messages } from '@salesforce/core'; import * as xml2js from 'xml2js'; import { Flow } from '../../../flow/Flow.js'; import { FlowParser, ParseTreeNode } from '../../../parse/FlowParser.js'; -// import { DefaultFtcFormatter } from '../../../format/DefaultFtcFormatter.js'; -import { JsFormatter } from '../../../format/JsFormatter.js'; +import { DefaultFtcFormatter } from '../../../format/DefaultFtcFormatter.js'; +import { TsFormatter } from '../../../format/TsFormatter.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('flowtocode', 'ftc.generate.code'); @@ -37,14 +37,29 @@ export default class FtcGenerateCode extends SfCommand { char: 'd', required: false, }), + 'output-format': Flags.string({ + summary: messages.getMessage('flags.output-format.summary'), + required: false, + default: 'ftc', + }) }; - private static getOutputPath(filepath: string, outputDir?: string): string { + private static getOutputPath(filepath: string, extension: string, outputDir?: string): string { if (outputDir) { - const filename = filepath.split('/').pop()?.replace('.flow-meta.xml', '.ftc') ?? 'flow.ftc'; + const filename = filepath.split('/').pop()?.replace('.flow-meta.xml', `.${extension}`) ?? `flow.${extension}`; return `${outputDir}/${filename}`; } - return filepath.replace('.flow-meta.xml', '.ftc'); + return filepath.replace('.flow-meta.xml', `.${extension}`); + } + + private static getFormatter(format: string): FormatterInterface { + if (format === 'ts') { + return new TsFormatter(); + } else if (format === 'ftc') { + return new DefaultFtcFormatter(); + } else { + throw new Error(`Unsupported format: ${format}. Supported formats are 'ftc' and 'ts'.`); + } } public async run(): Promise { @@ -55,11 +70,10 @@ export default class FtcGenerateCode extends SfCommand { const xmlParser: xml2js.Parser = new xml2js.Parser({ explicitArray: false, valueProcessors: [xml2js.processors.parseBooleans] }); const flow: Flow = ((await xmlParser.parseStringPromise(fileContent)) as ParsedXml).Flow; const flowParser: FlowParser = new FlowParser(); - const formatter: FormatterInterface = new JsFormatter(); // TODO: add format selection + const formatter: FormatterInterface = FtcGenerateCode.getFormatter(flags['output-format']); const treeNode: ParseTreeNode = flowParser.parse(flow); const parseTree: string = await formatter.convertToPseudocode(treeNode); - this.log(parseTree); // TODO: remove me - const outputPath: string = FtcGenerateCode.getOutputPath(filepath, flags.output); + const outputPath: string = FtcGenerateCode.getOutputPath(filepath, flags['output-format'], flags.output); await fs.writeFile(outputPath, parseTree, 'utf-8'); this.log(`Output written to ${outputPath}`); return { diff --git a/src/format/JsFormatter.ts b/src/format/TsFormatter.ts similarity index 81% rename from src/format/JsFormatter.ts rename to src/format/TsFormatter.ts index c552d06..466076b 100644 --- a/src/format/JsFormatter.ts +++ b/src/format/TsFormatter.ts @@ -4,7 +4,7 @@ import * as Flow from '../flow/Flow.js'; import { FormatterInterface } from '../commands/ftc/generate/code.js'; import { FlowAssignmentItem } from '../flow/Flow.js'; -export class JsFormatter implements FormatterInterface { +export class TsFormatter implements FormatterInterface { private functions: Map = new Map(); private revisitedElements: string[] = []; @@ -14,7 +14,7 @@ export class JsFormatter implements FormatterInterface { let description = `/** ${node.getFlowElement()?.description ?? ''} **/`; this.revisitedElements = this.filterNodes(node, n => n.getType() === NodeType.ALREADY_VISITED) - .map(n => n.getFlowElement()?.name ?? ''); + .map(n => n.getFlowElement()?.name ?? ''); if (node.getType() === NodeType.ROOT) { const flow = (node as RootNode).flow; @@ -32,17 +32,17 @@ export class JsFormatter implements FormatterInterface { const result = ` ${description} function ${flowName}(` - + variables.filter(v => v.isInput).map(v => `${v.name}: ${v.dataType}`).join(', ') - + ') {\n' - + variables.filter(v => !v.isInput).map(v => ` let ${v.name}: ${v.dataType} = ${v.value ? this.formatFlowElementReferenceOrValue(v.value) : 'null'};`).join('\n') - + '\n\n' - // + functions - + '\n\n' - + childrenCode - + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' - + '\n}'; + + variables.filter(v => v.isInput).map(v => `${v.name}: ${v.dataType}`).join(', ') + + ') {\n' + + variables.filter(v => !v.isInput).map(v => ` let ${v.name}: ${v.dataType} = ${v.value ? this.formatFlowElementReferenceOrValue(v.value) : 'null'};`).join('\n') + + '\n\n' + // + functions + + '\n\n' + + childrenCode + + 'return [' + variables.filter(v => v.isOutput).map(v => v.name).join(', ') + '];' + + '\n}'; // return Promise.resolve(result); - return prettier.format(result, {parser: 'babel-ts', printWidth: 300}); + return prettier.format(result, { parser: 'babel-ts', printWidth: 300 }); } private formatNodeChildren(node: ParseTreeNode): string { @@ -50,24 +50,24 @@ export class JsFormatter implements FormatterInterface { } private filterNodes(node: ParseTreeNode, callback: (node: ParseTreeNode) => boolean): ParseTreeNode[] { - const results: ParseTreeNode[] = []; - - const traverse = (currentNode: ParseTreeNode): void => { - if (callback(currentNode)) { - results.push(currentNode); - } - - for (const child of currentNode.getChildren()) { - traverse(child); - } - }; - - traverse(node); - return results; - } + const results: ParseTreeNode[] = []; + + const traverse = (currentNode: ParseTreeNode): void => { + if (callback(currentNode)) { + results.push(currentNode); + } + + for (const child of currentNode.getChildren()) { + traverse(child); + } + }; + + traverse(node); + return results; + } private formatNode(node: ParseTreeNode, sequenceNr: number = 0): string { - const flowElement = node.getFlowElement() as Flow.FlowElement; + const flowElement = node.getFlowElement() as Flow.FlowNode; switch (node.getType()) { case NodeType.TRY: @@ -93,7 +93,7 @@ export class JsFormatter implements FormatterInterface { case NodeType.ALREADY_VISITED: return this.formatAlreadyVisited(flowElement); default: - return this.formatNodeChain(node, `${flowElement.name}(); // ${JSON.stringify(flowElement)}`); + return this.formatNodeChain(node, `${flowElement.name}(); // TODO: ${flowElement.elementSubtype} ${JSON.stringify(flowElement)}`); } } @@ -101,7 +101,7 @@ export class JsFormatter implements FormatterInterface { if (node.getFlowElement() === null || node.getFlowElement() === undefined) { return nodeOwnBody; } - + const element = node.getFlowElement() as Flow.FlowElement; // Revisited nodes should be encapsulated as functions to call them from multiple places of the tree @@ -109,14 +109,14 @@ export class JsFormatter implements FormatterInterface { if (!this.functions.has(element.name)) { this.functions.set(element.name, nodeOwnBody); } - return `// [lbl] ${element.name}:\n${nodeOwnBody} \n ${this.formatNodeChildren(node)}`; + return `var label${element.name}; // label for GOTO \n${nodeOwnBody} \n ${this.formatNodeChildren(node)}`; } return nodeOwnBody + '\n' + this.formatNodeChildren(node); } private formatAlreadyVisited(element: Flow.FlowElement): string { - return `// goto ${element.name};`; + return `/* GOTO */ label${element.name};`; } private formatLoop(node: ParseTreeNode): string { @@ -141,13 +141,13 @@ export class JsFormatter implements FormatterInterface { private formatFlowScreen(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowScreen; - return this.formatNodeChain(node, `screen.show('${element.name}'); // Show ${element.label} ${element.description ?? ''}\n`); + return this.formatNodeChain(node, `screens.${element.name}.show(); // Show ${element.label} ${element.description ?? ''}\n`); } private formatDefaultOutcome(node: ParseTreeNode): string { return `else {\n${this.formatNodeChildren(node)}\n}`; // TODO: should it call formatNodeChain? } - + private formatAssignmentNode(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowAssignment; return this.formatNodeChain(node, `// ${element.label}\n${this.formatAssignments(element.assignmentItems ?? [])}`); @@ -192,7 +192,7 @@ export class JsFormatter implements FormatterInterface { private formatSubflow(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowSubflow; - return this.formatNodeChain(node, `subflow.call('${element.flowName}');\n`); // TODO: render parameters + return this.formatNodeChain(node, `subflows.${element.flowName}.call();\n`); // TODO: render parameters } private formatDecision(node: ParseTreeNode): string { @@ -206,8 +206,8 @@ export class JsFormatter implements FormatterInterface { let condition: string = element.conditionLogic === 'and' ? Array.from(conditions.keys()).map(key => key + 1).join(' AND ') : - element.conditionLogic === 'or' ? Array.from(conditions.keys()).map(key => key + 1).join(' OR ') : - element.conditionLogic; + element.conditionLogic === 'or' ? Array.from(conditions.keys()).map(key => key + 1).join(' OR ') : + element.conditionLogic; for (const [index, flowCondition] of conditions.entries()) { condition = condition.replace( @@ -236,7 +236,7 @@ export class JsFormatter implements FormatterInterface { private formatActionCall(node: ParseTreeNode): string { const element = node.getFlowElement() as Flow.FlowActionCall; const params = Array.isArray(element.inputParameters) ? element.inputParameters : [element.inputParameters]; - const body = `${element.actionType}.call('${element.actionName}', { + const body = `${element.actionType}.${element.actionName}.call({ ${params.map(param => param.name + ': ' + this.formatFlowElementReferenceOrValue(param.value)).join(', ')} }); // ${element.label} ${element.description ?? ''}`; return this.formatNodeChain(node, body); diff --git a/test/resources/test.flow.expected.js b/test/resources/test.flow.expected.js deleted file mode 100644 index 7a7bfe8ab65564ed14462bc0b2024b25263d85b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2130 zcmbW2PjAye5XI+=#CNcA3Z-)HtvHmHNR^O)Hn$2{CUr`{b-+m*D%7tI{AM@n-Hp2? zimZ)l@BDf5-pusxpOGB!Wb#s8@avz6RI-#6U&d0)Le|e%%r+J|)*5YOY-e&OjZBF# z$G_q3R!aVEWGELzpYpYpYdPl}aeIknYwmC474N^hj5XGZSq^&$)bqH3^p(Ef5T93(JM>FzTRbZAyrg)}NZueff4fCC!PSYRwT(wrcYhHEQ`Db-mW^>Q}GyM4u9&VJ_yd zq