feat: add flow generation script and example flow documentation
This commit is contained in:
parent
ccc27ddcb5
commit
319229830e
4 changed files with 210 additions and 3 deletions
37
apps/infoalloggi/docs/flows/example.md
Normal file
37
apps/infoalloggi/docs/flows/example.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Flow Example
|
||||
|
||||
```js
|
||||
// FLOWS: example | 1 | Root Node
|
||||
|
||||
// FLOWS: example | 2 | Test
|
||||
|
||||
// FLOWS: example | 3 | Test2
|
||||
|
||||
// FLOWS: example | 3.1 | TestBranch
|
||||
|
||||
// FLOWS: example | 3.2 | SecondBranch
|
||||
|
||||
// FLOWS: example | 3.1.1 | TestBranchSub
|
||||
|
||||
// FLOWS: example | 3.1.2 | TestBranchBranchSub
|
||||
|
||||
// FLOWS: example | 3.2.1 | SecondBranchSub
|
||||
|
||||
// FLOWS: example | 3.3 | MergeBack
|
||||
```
|
||||
|
||||
```mermaid
|
||||
|
||||
graph TD
|
||||
step_1["Root Node"] --> step_2["Test"]
|
||||
step_2["Test"] --> step_3["Test2"]
|
||||
step_3["Test2"] --> step_3_1["TestBranch"]
|
||||
step_3["Test2"] --> step_3_2["SecondBranch"]
|
||||
step_3["Test2"] --> step_3_3["MergeBack"]
|
||||
step_3_1["TestBranch"] --> step_3_1_1["TestBranchSub"]
|
||||
step_3_1["TestBranch"] --> step_3_1_2["TestBranchBranchSub"]
|
||||
step_3_1["TestBranch"] --> step_3_2["SecondBranch"]
|
||||
step_3_1_1["TestBranchSub"] --> step_3_1_2["TestBranchBranchSub"]
|
||||
step_3_2["SecondBranch"] --> step_3_2_1["SecondBranchSub"]
|
||||
step_3_2["SecondBranch"] --> step_3_3["MergeBack"]
|
||||
```
|
||||
168
apps/infoalloggi/flow-gen.ts
Normal file
168
apps/infoalloggi/flow-gen.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
interface FlowStep {
|
||||
id: string;
|
||||
label: string;
|
||||
major: number;
|
||||
level: number;
|
||||
file: string; // New: Track file path
|
||||
line: number; // New: Track line number
|
||||
}
|
||||
|
||||
interface FlowMap {
|
||||
[flowId: string]: FlowStep[];
|
||||
}
|
||||
|
||||
/** CONFIGURATION **/
|
||||
const SRC_DIR = "./src";
|
||||
const OUTPUT_DIR = "./docs/flows";
|
||||
const FILE_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx"];
|
||||
|
||||
// Updated Regex: Handles //FLOWS and // FLOWS
|
||||
const FLOW_REGEX = /\/\/\s*FLOWS:\s*([^|]+)\s*\|\s*([\w.]+)\s*\|\s*(.*)/g;
|
||||
|
||||
/** UTILS **/
|
||||
function getFiles(dir: string, allFiles: string[] = []): string[] {
|
||||
if (!fs.existsSync(dir)) return allFiles;
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const name = path.join(dir, file);
|
||||
if (fs.statSync(name).isDirectory()) getFiles(name, allFiles);
|
||||
else if (FILE_EXTENSIONS.includes(path.extname(name))) allFiles.push(name);
|
||||
}
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
function safeId(id: string): string {
|
||||
// Mermaid IDs cannot contain dots
|
||||
return `step_${id.replace(/\./g, "_")}`;
|
||||
}
|
||||
|
||||
/** CORE LOGIC **/
|
||||
function main() {
|
||||
const flowData: FlowMap = {};
|
||||
const files = getFiles(SRC_DIR);
|
||||
|
||||
console.log(`🔍 Scanning ${files.length} files for // FLOWS comments...`);
|
||||
|
||||
// 1. Extraction
|
||||
files.forEach((file) => {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
lines.forEach((lineText, index) => {
|
||||
let match: RegExpExecArray | null;
|
||||
FLOW_REGEX.lastIndex = 0;
|
||||
match = FLOW_REGEX.exec(lineText);
|
||||
while (match !== null) {
|
||||
const [_, flowId, id, label] = match.map((s) => s.trim());
|
||||
if (!flowId || !id || !label) {
|
||||
console.warn(`⚠️ Invalid FLOWS comment in ${file}: ${match[0]}`);
|
||||
continue;
|
||||
}
|
||||
if (!flowData[flowId]) flowData[flowId] = [];
|
||||
|
||||
flowData[flowId].push({
|
||||
id,
|
||||
label,
|
||||
major: parseInt(id.split(".")?.[0] || "0"),
|
||||
level: id.split(".").length,
|
||||
file: path.relative(process.cwd(), file), // Store relative path
|
||||
line: index + 1, // Store line number (1-based)
|
||||
});
|
||||
match = FLOW_REGEX.exec(lineText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (Object.keys(flowData).length === 0) {
|
||||
console.log(
|
||||
"⚠️ No flow comments found. Try adding: // FLOWS: myflow | 1 | Start",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Generate Output
|
||||
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
for (const [flowId, steps] of Object.entries(flowData)) {
|
||||
// Sort logically (1, 1.1, 1.2, 2)
|
||||
const sorted = steps.sort((a, b) =>
|
||||
a.id.localeCompare(b.id, undefined, { numeric: true }),
|
||||
);
|
||||
const mermaid = ["graph TD"];
|
||||
const connections = new Set<string>();
|
||||
|
||||
sorted.forEach((current) => {
|
||||
const nextSteps = sorted.filter((candidate) => {
|
||||
const currentIdPrefix = `${current.id}.`;
|
||||
|
||||
// 1. DESCEND: Link to immediate children (3 -> 3.1, 3 -> 3.2)
|
||||
// Check if candidate starts with current ID and is exactly one level deeper
|
||||
if (
|
||||
candidate.id.startsWith(currentIdPrefix) &&
|
||||
candidate.level === current.level + 1
|
||||
) {
|
||||
// Ensure it's a direct child (e.g., 3 -> 3.1, not 3 -> 3.1.1)
|
||||
const remaining = candidate.id.replace(currentIdPrefix, "");
|
||||
return !remaining.includes(".");
|
||||
}
|
||||
|
||||
// 2. MERGE: Link leaf nodes to the next major integer (3.1.2 -> 4, 3.2.1 -> 4)
|
||||
const isLeaf = !sorted.some(
|
||||
(s) => s.id.startsWith(currentIdPrefix) && s.id !== current.id,
|
||||
);
|
||||
if (
|
||||
isLeaf &&
|
||||
candidate.major === current.major + 1 &&
|
||||
candidate.level === 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. SEQUENCE (Optional): If you want 3.1.1 -> 3.1.2
|
||||
// If they share the exact same parent and are consecutive
|
||||
const currentParent = current.id.includes(".")
|
||||
? current.id.substring(0, current.id.lastIndexOf("."))
|
||||
: null;
|
||||
const candParent = candidate.id.includes(".")
|
||||
? candidate.id.substring(0, candidate.id.lastIndexOf("."))
|
||||
: null;
|
||||
|
||||
if (
|
||||
currentParent !== null &&
|
||||
currentParent === candParent &&
|
||||
candidate.level === current.level
|
||||
) {
|
||||
// Only link if you want branches to be sequential.
|
||||
// In your case, you likely want 3.1.1 and 3.1.2 to both go to 4 independently.
|
||||
// So we leave this FALSE to keep them as parallel paths.
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
nextSteps.forEach((next) => {
|
||||
const link = ` ${safeId(current.id)}["${current.label}"] --> ${safeId(next.id)}["${next.label}"]`;
|
||||
connections.add(link);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle single-node flows (start node with no connections)
|
||||
if (connections.size === 0 && sorted.length > 0 && sorted[0]) {
|
||||
mermaid.push(` ${safeId(sorted[0].id)}["${sorted[0].label}"]`);
|
||||
} else {
|
||||
connections.forEach((c) => mermaid.push(c));
|
||||
}
|
||||
|
||||
const outPath = path.join(OUTPUT_DIR, `${flowId}.md`);
|
||||
|
||||
const header = "```mermaid\n\n";
|
||||
const footer = "\n```";
|
||||
fs.writeFileSync(outPath, header + mermaid.join("\n") + footer);
|
||||
console.log(`✅ Generated: ${outPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
"test": " npm run lint && npm run types ",
|
||||
"types": "tsc --noEmit",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"vitest": "vitest run"
|
||||
"vitest": "vitest run",
|
||||
"flow": "ts-node flow-gen.ts"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
|
|
@ -138,4 +139,4 @@
|
|||
"ct3aMetadata": {
|
||||
"initVersion": "7.9.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,8 @@
|
|||
"public",
|
||||
"emails",
|
||||
".next/types/**/*.ts",
|
||||
"tests"
|
||||
"tests",
|
||||
"flow-gen.ts"
|
||||
],
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue