@workflow/builders 4.0.1-beta.6 → 4.0.1-beta.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apply-swc-transform.d.ts +14 -4
- package/dist/apply-swc-transform.d.ts.map +1 -1
- package/dist/apply-swc-transform.js +59 -9
- package/dist/apply-swc-transform.js.map +1 -1
- package/dist/base-builder.d.ts +65 -22
- package/dist/base-builder.d.ts.map +1 -1
- package/dist/base-builder.js +482 -136
- package/dist/base-builder.js.map +1 -1
- package/dist/build-queue.d.ts +18 -0
- package/dist/build-queue.d.ts.map +1 -0
- package/dist/build-queue.js +26 -0
- package/dist/build-queue.js.map +1 -0
- package/dist/config-helpers.d.ts +31 -0
- package/dist/config-helpers.d.ts.map +1 -1
- package/dist/config-helpers.js +60 -0
- package/dist/config-helpers.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/discover-entries-esbuild-plugin.d.ts +2 -3
- package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
- package/dist/discover-entries-esbuild-plugin.js +71 -26
- package/dist/discover-entries-esbuild-plugin.js.map +1 -1
- package/dist/get-input-files.test.d.ts +2 -0
- package/dist/get-input-files.test.d.ts.map +1 -0
- package/dist/get-input-files.test.js +119 -0
- package/dist/get-input-files.test.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/module-specifier.d.ts +85 -0
- package/dist/module-specifier.d.ts.map +1 -0
- package/dist/module-specifier.js +448 -0
- package/dist/module-specifier.js.map +1 -0
- package/dist/module-specifier.test.d.ts +2 -0
- package/dist/module-specifier.test.d.ts.map +1 -0
- package/dist/module-specifier.test.js +211 -0
- package/dist/module-specifier.test.js.map +1 -0
- package/dist/node-module-esbuild-plugin.d.ts +37 -0
- package/dist/node-module-esbuild-plugin.d.ts.map +1 -1
- package/dist/node-module-esbuild-plugin.js +286 -7
- package/dist/node-module-esbuild-plugin.js.map +1 -1
- package/dist/node-module-esbuild-plugin.test.js +347 -146
- package/dist/node-module-esbuild-plugin.test.js.map +1 -1
- package/dist/pseudo-package-esbuild-plugin.d.ts +20 -0
- package/dist/pseudo-package-esbuild-plugin.d.ts.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.js +47 -0
- package/dist/pseudo-package-esbuild-plugin.js.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.test.d.ts +2 -0
- package/dist/pseudo-package-esbuild-plugin.test.d.ts.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.test.js +315 -0
- package/dist/pseudo-package-esbuild-plugin.test.js.map +1 -0
- package/dist/request-converter.d.ts +3 -0
- package/dist/request-converter.d.ts.map +1 -0
- package/dist/request-converter.js +14 -0
- package/dist/request-converter.js.map +1 -0
- package/dist/standalone.d.ts +3 -0
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +37 -13
- package/dist/standalone.js.map +1 -1
- package/dist/swc-esbuild-plugin.d.ts +0 -2
- package/dist/swc-esbuild-plugin.d.ts.map +1 -1
- package/dist/swc-esbuild-plugin.js +33 -15
- package/dist/swc-esbuild-plugin.js.map +1 -1
- package/dist/transform-utils.d.ts +68 -0
- package/dist/transform-utils.d.ts.map +1 -0
- package/dist/transform-utils.js +102 -0
- package/dist/transform-utils.js.map +1 -0
- package/dist/transform-utils.test.d.ts +2 -0
- package/dist/transform-utils.test.d.ts.map +1 -0
- package/dist/transform-utils.test.js +283 -0
- package/dist/transform-utils.test.js.map +1 -0
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/vercel-build-output-api.d.ts.map +1 -1
- package/dist/vercel-build-output-api.js +36 -14
- package/dist/vercel-build-output-api.js.map +1 -1
- package/dist/workflow-alias.d.ts +3 -0
- package/dist/workflow-alias.d.ts.map +1 -0
- package/dist/workflow-alias.js +46 -0
- package/dist/workflow-alias.js.map +1 -0
- package/dist/workflow-alias.test.d.ts +2 -0
- package/dist/workflow-alias.test.d.ts.map +1 -0
- package/dist/workflow-alias.test.js +46 -0
- package/dist/workflow-alias.test.js.map +1 -0
- package/dist/workflows-extractor.d.ts +92 -0
- package/dist/workflows-extractor.d.ts.map +1 -0
- package/dist/workflows-extractor.js +1476 -0
- package/dist/workflows-extractor.js.map +1 -0
- package/package.json +12 -11
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseSync } from '@swc/core';
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
/**
|
|
7
|
+
* Workflow primitives that should be shown as nodes in the graph.
|
|
8
|
+
* These are built-in workflow functions that represent meaningful
|
|
9
|
+
* pauses or wait points in the workflow execution.
|
|
10
|
+
*/
|
|
11
|
+
const WORKFLOW_PRIMITIVES = new Set(['sleep', 'createHook', 'createWebhook']);
|
|
12
|
+
/**
|
|
13
|
+
* Extract the original function name from a stepId.
|
|
14
|
+
* stepId format: "step//path/to/file.ts//functionName"
|
|
15
|
+
* The bundler may rename functions to avoid collisions (e.g. add -> add2),
|
|
16
|
+
* but the stepId contains the original TypeScript function name.
|
|
17
|
+
*/
|
|
18
|
+
function getOriginalStepName(stepId, fallbackName) {
|
|
19
|
+
const parts = stepId.split('//');
|
|
20
|
+
return parts.length > 2 ? parts[2] : fallbackName;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract a readable condition text from an Expression AST node.
|
|
24
|
+
* Recursively builds a string representation of the condition.
|
|
25
|
+
*/
|
|
26
|
+
function getConditionText(expr) {
|
|
27
|
+
switch (expr.type) {
|
|
28
|
+
case 'Identifier':
|
|
29
|
+
return expr.value;
|
|
30
|
+
case 'BooleanLiteral':
|
|
31
|
+
return String(expr.value);
|
|
32
|
+
case 'NumericLiteral':
|
|
33
|
+
return String(expr.value);
|
|
34
|
+
case 'StringLiteral':
|
|
35
|
+
return `"${expr.value}"`;
|
|
36
|
+
case 'BinaryExpression': {
|
|
37
|
+
const bin = expr;
|
|
38
|
+
const left = getConditionText(bin.left);
|
|
39
|
+
const right = getConditionText(bin.right);
|
|
40
|
+
return `${left} ${bin.operator} ${right}`;
|
|
41
|
+
}
|
|
42
|
+
case 'UnaryExpression': {
|
|
43
|
+
const unary = expr;
|
|
44
|
+
const arg = getConditionText(unary.argument);
|
|
45
|
+
return `${unary.operator}${arg}`;
|
|
46
|
+
}
|
|
47
|
+
case 'MemberExpression': {
|
|
48
|
+
const member = expr;
|
|
49
|
+
const obj = getConditionText(member.object);
|
|
50
|
+
if (member.property.type === 'Identifier') {
|
|
51
|
+
return `${obj}.${member.property.value}`;
|
|
52
|
+
}
|
|
53
|
+
if (member.property.type === 'Computed') {
|
|
54
|
+
const computed = member.property.expression;
|
|
55
|
+
return `${obj}[${getConditionText(computed)}]`;
|
|
56
|
+
}
|
|
57
|
+
return obj;
|
|
58
|
+
}
|
|
59
|
+
case 'CallExpression': {
|
|
60
|
+
const call = expr;
|
|
61
|
+
const callee = call.callee;
|
|
62
|
+
// Handle callee which could be Expression, Super, or Import
|
|
63
|
+
if (callee.type === 'Super' || callee.type === 'Import') {
|
|
64
|
+
return `${callee.type.toLowerCase()}()`;
|
|
65
|
+
}
|
|
66
|
+
return `${getConditionText(callee)}()`;
|
|
67
|
+
}
|
|
68
|
+
case 'ParenthesisExpression': {
|
|
69
|
+
const paren = expr;
|
|
70
|
+
return `(${getConditionText(paren.expression)})`;
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return 'condition';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Extraction Functions
|
|
78
|
+
// =============================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Extracts workflow graphs from a bundled workflow file.
|
|
81
|
+
* Returns workflow entries organized by file path, ready for merging into Manifest.
|
|
82
|
+
*/
|
|
83
|
+
export async function extractWorkflowGraphs(bundlePath) {
|
|
84
|
+
const bundleCode = await readFile(bundlePath, 'utf-8');
|
|
85
|
+
try {
|
|
86
|
+
let actualWorkflowCode = bundleCode;
|
|
87
|
+
const bundleAst = parseSync(bundleCode, {
|
|
88
|
+
syntax: 'ecmascript',
|
|
89
|
+
target: 'es2022',
|
|
90
|
+
});
|
|
91
|
+
const workflowCodeValue = extractWorkflowCodeFromBundle(bundleAst);
|
|
92
|
+
if (workflowCodeValue) {
|
|
93
|
+
actualWorkflowCode = workflowCodeValue;
|
|
94
|
+
}
|
|
95
|
+
const ast = parseSync(actualWorkflowCode, {
|
|
96
|
+
syntax: 'ecmascript',
|
|
97
|
+
target: 'es2022',
|
|
98
|
+
});
|
|
99
|
+
const stepDeclarations = extractStepDeclarations(actualWorkflowCode);
|
|
100
|
+
const functionMap = buildFunctionMap(ast, stepDeclarations);
|
|
101
|
+
const variableMap = buildVariableMap(ast);
|
|
102
|
+
return extractWorkflows(ast, stepDeclarations, functionMap, variableMap);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('Failed to extract workflow graphs from bundle:', error);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Extract the workflowCode string value from a parsed bundle AST
|
|
111
|
+
*/
|
|
112
|
+
function extractWorkflowCodeFromBundle(ast) {
|
|
113
|
+
for (const item of ast.body) {
|
|
114
|
+
if (item.type === 'VariableDeclaration') {
|
|
115
|
+
for (const decl of item.declarations) {
|
|
116
|
+
if (decl.id.type === 'Identifier' &&
|
|
117
|
+
decl.id.value === 'workflowCode' &&
|
|
118
|
+
decl.init) {
|
|
119
|
+
if (decl.init.type === 'TemplateLiteral') {
|
|
120
|
+
return decl.init.quasis.map((q) => q.cooked || q.raw).join('');
|
|
121
|
+
}
|
|
122
|
+
if (decl.init.type === 'StringLiteral') {
|
|
123
|
+
return decl.init.value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract step declarations using regex for speed
|
|
133
|
+
*/
|
|
134
|
+
function extractStepDeclarations(bundleCode) {
|
|
135
|
+
const stepDeclarations = new Map();
|
|
136
|
+
const stepPattern = /var (\w+) = globalThis\[Symbol\.for\("WORKFLOW_USE_STEP"\)\]\("([^"]+)"\)/g;
|
|
137
|
+
const lines = bundleCode.split('\n');
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
stepPattern.lastIndex = 0;
|
|
140
|
+
const match = stepPattern.exec(line);
|
|
141
|
+
if (match) {
|
|
142
|
+
const [, varName, stepId] = match;
|
|
143
|
+
stepDeclarations.set(varName, { stepId });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return stepDeclarations;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extract inline step declarations from within a function body.
|
|
150
|
+
* These are steps defined as variable declarations inside a workflow function.
|
|
151
|
+
* Pattern: var/const varName = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("stepId")
|
|
152
|
+
*/
|
|
153
|
+
function extractInlineStepDeclarations(stmts) {
|
|
154
|
+
const inlineSteps = new Map();
|
|
155
|
+
for (const stmt of stmts) {
|
|
156
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
157
|
+
const varDecl = stmt;
|
|
158
|
+
for (const decl of varDecl.declarations) {
|
|
159
|
+
if (decl.id.type === 'Identifier' &&
|
|
160
|
+
decl.init?.type === 'CallExpression') {
|
|
161
|
+
const callExpr = decl.init;
|
|
162
|
+
// Check for globalThis[Symbol.for("WORKFLOW_USE_STEP")]("stepId") pattern
|
|
163
|
+
if (callExpr.callee.type === 'MemberExpression') {
|
|
164
|
+
const member = callExpr.callee;
|
|
165
|
+
// Check if object is globalThis
|
|
166
|
+
if (member.object.type === 'Identifier' &&
|
|
167
|
+
member.object.value === 'globalThis' &&
|
|
168
|
+
member.property.type === 'Computed') {
|
|
169
|
+
// For computed member access globalThis[Symbol.for(...)],
|
|
170
|
+
// the property is a Computed type containing the expression
|
|
171
|
+
const computedExpr = member.property.expression;
|
|
172
|
+
if (computedExpr?.type === 'CallExpression') {
|
|
173
|
+
const symbolCall = computedExpr;
|
|
174
|
+
// Check if it's Symbol.for("WORKFLOW_USE_STEP")
|
|
175
|
+
if (symbolCall.callee.type === 'MemberExpression') {
|
|
176
|
+
const symbolMember = symbolCall.callee;
|
|
177
|
+
if (symbolMember.object.type === 'Identifier' &&
|
|
178
|
+
symbolMember.object.value === 'Symbol' &&
|
|
179
|
+
symbolMember.property.type === 'Identifier' &&
|
|
180
|
+
symbolMember.property.value === 'for' &&
|
|
181
|
+
symbolCall.arguments.length > 0 &&
|
|
182
|
+
symbolCall.arguments[0].expression.type ===
|
|
183
|
+
'StringLiteral' &&
|
|
184
|
+
symbolCall.arguments[0].expression.value ===
|
|
185
|
+
'WORKFLOW_USE_STEP') {
|
|
186
|
+
// Extract the stepId from the outer call arguments
|
|
187
|
+
if (callExpr.arguments.length > 0 &&
|
|
188
|
+
callExpr.arguments[0].expression.type === 'StringLiteral') {
|
|
189
|
+
const stepId = callExpr.arguments[0].expression
|
|
190
|
+
.value;
|
|
191
|
+
const varName = decl.id.value;
|
|
192
|
+
inlineSteps.set(varName, { stepId });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return inlineSteps;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build a map of all functions in the bundle for transitive step resolution
|
|
207
|
+
*/
|
|
208
|
+
function buildFunctionMap(ast, stepDeclarations) {
|
|
209
|
+
const functionMap = new Map();
|
|
210
|
+
for (const item of ast.body) {
|
|
211
|
+
if (item.type === 'FunctionDeclaration') {
|
|
212
|
+
const func = item;
|
|
213
|
+
if (func.identifier) {
|
|
214
|
+
const name = func.identifier.value;
|
|
215
|
+
const isStep = stepDeclarations.has(name);
|
|
216
|
+
functionMap.set(name, {
|
|
217
|
+
name,
|
|
218
|
+
body: func.body,
|
|
219
|
+
isStep,
|
|
220
|
+
stepId: isStep ? stepDeclarations.get(name)?.stepId : undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (item.type === 'VariableDeclaration') {
|
|
225
|
+
const varDecl = item;
|
|
226
|
+
for (const decl of varDecl.declarations) {
|
|
227
|
+
if (decl.id.type === 'Identifier' && decl.init) {
|
|
228
|
+
const name = decl.id.value;
|
|
229
|
+
const isStep = stepDeclarations.has(name);
|
|
230
|
+
if (decl.init.type === 'FunctionExpression') {
|
|
231
|
+
const funcExpr = decl.init;
|
|
232
|
+
functionMap.set(name, {
|
|
233
|
+
name,
|
|
234
|
+
body: funcExpr.body,
|
|
235
|
+
isStep,
|
|
236
|
+
stepId: isStep ? stepDeclarations.get(name)?.stepId : undefined,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else if (decl.init.type === 'ArrowFunctionExpression') {
|
|
240
|
+
const arrowFunc = decl.init;
|
|
241
|
+
functionMap.set(name, {
|
|
242
|
+
name,
|
|
243
|
+
body: arrowFunc.body,
|
|
244
|
+
isStep,
|
|
245
|
+
stepId: isStep ? stepDeclarations.get(name)?.stepId : undefined,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return functionMap;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Build a map of variable definitions (objects) for tool resolution
|
|
256
|
+
* This allows us to resolve tools objects to the actual tools object
|
|
257
|
+
*/
|
|
258
|
+
function buildVariableMap(ast) {
|
|
259
|
+
const variableMap = new Map();
|
|
260
|
+
for (const item of ast.body) {
|
|
261
|
+
if (item.type === 'VariableDeclaration') {
|
|
262
|
+
const varDecl = item;
|
|
263
|
+
for (const decl of varDecl.declarations) {
|
|
264
|
+
if (decl.type === 'VariableDeclarator' &&
|
|
265
|
+
decl.id.type === 'Identifier' &&
|
|
266
|
+
decl.init?.type === 'ObjectExpression') {
|
|
267
|
+
variableMap.set(decl.id.value, decl.init);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return variableMap;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Extract workflows from AST
|
|
276
|
+
*/
|
|
277
|
+
function extractWorkflows(ast, stepDeclarations, functionMap, variableMap) {
|
|
278
|
+
const result = {};
|
|
279
|
+
for (const item of ast.body) {
|
|
280
|
+
if (item.type === 'FunctionDeclaration') {
|
|
281
|
+
const func = item;
|
|
282
|
+
if (!func.identifier)
|
|
283
|
+
continue;
|
|
284
|
+
const workflowName = func.identifier.value;
|
|
285
|
+
const workflowId = findWorkflowId(ast, workflowName);
|
|
286
|
+
if (!workflowId)
|
|
287
|
+
continue;
|
|
288
|
+
// Extract file path and actual workflow name from workflowId: "workflow//path/to/file.ts//functionName"
|
|
289
|
+
// The bundler may rename functions to avoid collisions (e.g. addTenWorkflow -> addTenWorkflow2),
|
|
290
|
+
// but the workflowId contains the original TypeScript function name.
|
|
291
|
+
const parts = workflowId.split('//');
|
|
292
|
+
const filePath = parts.length > 1 ? parts[1] : 'unknown';
|
|
293
|
+
const actualWorkflowName = parts.length > 2 ? parts[2] : workflowName;
|
|
294
|
+
const graph = analyzeWorkflowFunction(func, workflowName, stepDeclarations, functionMap, variableMap);
|
|
295
|
+
if (!result[filePath]) {
|
|
296
|
+
result[filePath] = {};
|
|
297
|
+
}
|
|
298
|
+
result[filePath][actualWorkflowName] = {
|
|
299
|
+
workflowId,
|
|
300
|
+
graph,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Find workflowId assignment for a function
|
|
308
|
+
*/
|
|
309
|
+
function findWorkflowId(ast, functionName) {
|
|
310
|
+
for (const item of ast.body) {
|
|
311
|
+
if (item.type === 'ExpressionStatement') {
|
|
312
|
+
const expr = item.expression;
|
|
313
|
+
if (expr.type === 'AssignmentExpression') {
|
|
314
|
+
const left = expr.left;
|
|
315
|
+
if (left.type === 'MemberExpression') {
|
|
316
|
+
const obj = left.object;
|
|
317
|
+
const prop = left.property;
|
|
318
|
+
if (obj.type === 'Identifier' &&
|
|
319
|
+
obj.value === functionName &&
|
|
320
|
+
prop.type === 'Identifier' &&
|
|
321
|
+
prop.value === 'workflowId') {
|
|
322
|
+
const right = expr.right;
|
|
323
|
+
if (right.type === 'StringLiteral') {
|
|
324
|
+
return right.value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Analyze a workflow function and build its graph
|
|
335
|
+
*/
|
|
336
|
+
function analyzeWorkflowFunction(func, workflowName, stepDeclarations, functionMap, variableMap) {
|
|
337
|
+
const nodes = [];
|
|
338
|
+
const edges = [];
|
|
339
|
+
// Add start node
|
|
340
|
+
nodes.push({
|
|
341
|
+
id: 'start',
|
|
342
|
+
type: 'workflowStart',
|
|
343
|
+
data: {
|
|
344
|
+
label: `Start: ${workflowName}`,
|
|
345
|
+
nodeKind: 'workflow_start',
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
const context = {
|
|
349
|
+
parallelCounter: 0,
|
|
350
|
+
loopCounter: 0,
|
|
351
|
+
conditionalCounter: 0,
|
|
352
|
+
nodeCounter: 0,
|
|
353
|
+
inLoop: null,
|
|
354
|
+
inConditional: null,
|
|
355
|
+
webhookVariables: new Set(),
|
|
356
|
+
promiseArrays: new Map(),
|
|
357
|
+
};
|
|
358
|
+
let prevExitIds = ['start'];
|
|
359
|
+
if (func.body?.stmts) {
|
|
360
|
+
// Extract inline step declarations from the workflow body
|
|
361
|
+
// These are steps defined as variables inside the workflow function
|
|
362
|
+
const inlineSteps = extractInlineStepDeclarations(func.body.stmts);
|
|
363
|
+
// Merge inline steps with global step declarations
|
|
364
|
+
const mergedStepDeclarations = new Map(stepDeclarations);
|
|
365
|
+
for (const [name, info] of inlineSteps) {
|
|
366
|
+
mergedStepDeclarations.set(name, info);
|
|
367
|
+
}
|
|
368
|
+
for (const stmt of func.body.stmts) {
|
|
369
|
+
const result = analyzeStatement(stmt, mergedStepDeclarations, context, functionMap, variableMap);
|
|
370
|
+
nodes.push(...result.nodes);
|
|
371
|
+
edges.push(...result.edges);
|
|
372
|
+
for (const prevId of prevExitIds) {
|
|
373
|
+
for (const entryId of result.entryNodeIds) {
|
|
374
|
+
const edgeId = `e_${prevId}_${entryId}`;
|
|
375
|
+
if (!edges.find((e) => e.id === edgeId)) {
|
|
376
|
+
const targetNode = result.nodes.find((n) => n.id === entryId);
|
|
377
|
+
// Only use 'parallel' type for parallel group connections
|
|
378
|
+
// Sequential connections (including to/from loops) should be 'default'
|
|
379
|
+
const edgeType = targetNode?.metadata?.parallelGroupId
|
|
380
|
+
? 'parallel'
|
|
381
|
+
: 'default';
|
|
382
|
+
edges.push({
|
|
383
|
+
id: edgeId,
|
|
384
|
+
source: prevId,
|
|
385
|
+
target: entryId,
|
|
386
|
+
type: edgeType,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (result.exitNodeIds.length > 0) {
|
|
392
|
+
prevExitIds = result.exitNodeIds;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Add end node
|
|
397
|
+
nodes.push({
|
|
398
|
+
id: 'end',
|
|
399
|
+
type: 'workflowEnd',
|
|
400
|
+
data: {
|
|
401
|
+
label: 'Return',
|
|
402
|
+
nodeKind: 'workflow_end',
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
for (const prevId of prevExitIds) {
|
|
406
|
+
edges.push({
|
|
407
|
+
id: `e_${prevId}_end`,
|
|
408
|
+
source: prevId,
|
|
409
|
+
target: 'end',
|
|
410
|
+
type: 'default',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return { nodes, edges };
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Check if a statement or block contains await expressions (recursively)
|
|
417
|
+
* Used to determine if a for/while loop is truly a looping execution pattern
|
|
418
|
+
* vs just collecting promises for parallel execution
|
|
419
|
+
*/
|
|
420
|
+
function containsAwaitExpression(node) {
|
|
421
|
+
if (!node)
|
|
422
|
+
return false;
|
|
423
|
+
// Direct await expression
|
|
424
|
+
if (node.type === 'AwaitExpression')
|
|
425
|
+
return true;
|
|
426
|
+
// Check block statements
|
|
427
|
+
if (node.type === 'BlockStatement' && node.stmts) {
|
|
428
|
+
return node.stmts.some((stmt) => containsAwaitExpression(stmt));
|
|
429
|
+
}
|
|
430
|
+
// Check expression statements
|
|
431
|
+
if (node.type === 'ExpressionStatement' && node.expression) {
|
|
432
|
+
return containsAwaitExpression(node.expression);
|
|
433
|
+
}
|
|
434
|
+
// Check variable declarations
|
|
435
|
+
if (node.type === 'VariableDeclaration' && node.declarations) {
|
|
436
|
+
return node.declarations.some((decl) => decl.init && containsAwaitExpression(decl.init));
|
|
437
|
+
}
|
|
438
|
+
// Check if statements
|
|
439
|
+
if (node.type === 'IfStatement') {
|
|
440
|
+
return (containsAwaitExpression(node.consequent) ||
|
|
441
|
+
containsAwaitExpression(node.alternate));
|
|
442
|
+
}
|
|
443
|
+
// Check for statements
|
|
444
|
+
if (node.type === 'ForStatement' ||
|
|
445
|
+
node.type === 'WhileStatement' ||
|
|
446
|
+
node.type === 'ForOfStatement' ||
|
|
447
|
+
node.type === 'ForInStatement') {
|
|
448
|
+
return containsAwaitExpression(node.body);
|
|
449
|
+
}
|
|
450
|
+
// Check assignment expressions (e.g., result = await doWork())
|
|
451
|
+
if (node.type === 'AssignmentExpression') {
|
|
452
|
+
return containsAwaitExpression(node.right);
|
|
453
|
+
}
|
|
454
|
+
// Check call expressions (for await in arguments)
|
|
455
|
+
if (node.type === 'CallExpression') {
|
|
456
|
+
if (node.arguments) {
|
|
457
|
+
return node.arguments.some((arg) => containsAwaitExpression(arg.expression || arg));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Analyze a statement and extract step calls with proper CFG structure
|
|
464
|
+
*/
|
|
465
|
+
function analyzeStatement(stmt, stepDeclarations, context, functionMap, variableMap) {
|
|
466
|
+
const nodes = [];
|
|
467
|
+
const edges = [];
|
|
468
|
+
let entryNodeIds = [];
|
|
469
|
+
let exitNodeIds = [];
|
|
470
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
471
|
+
const varDecl = stmt;
|
|
472
|
+
for (const decl of varDecl.declarations) {
|
|
473
|
+
if (decl.init) {
|
|
474
|
+
// Track webhook/hook variable assignments: const webhook = createWebhook()
|
|
475
|
+
if (decl.id.type === 'Identifier' &&
|
|
476
|
+
decl.init.type === 'CallExpression' &&
|
|
477
|
+
decl.init.callee.type === 'Identifier') {
|
|
478
|
+
const funcName = decl.init.callee
|
|
479
|
+
.value;
|
|
480
|
+
if (funcName === 'createWebhook' || funcName === 'createHook') {
|
|
481
|
+
context.webhookVariables.add(decl.id.value);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Track empty array assignments for Promise.all pattern: const promises = []
|
|
485
|
+
if (decl.id.type === 'Identifier' &&
|
|
486
|
+
decl.init.type === 'ArrayExpression') {
|
|
487
|
+
const elements = decl.init.elements;
|
|
488
|
+
// Empty array: elements is undefined, null, or empty array
|
|
489
|
+
if (!elements || elements.length === 0) {
|
|
490
|
+
const varName = decl.id.value;
|
|
491
|
+
context.promiseArrays.set(varName, []);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const result = analyzeExpression(decl.init, stepDeclarations, context, functionMap, variableMap);
|
|
495
|
+
nodes.push(...result.nodes);
|
|
496
|
+
edges.push(...result.edges);
|
|
497
|
+
if (entryNodeIds.length === 0) {
|
|
498
|
+
entryNodeIds = result.entryNodeIds;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
for (const prevId of exitNodeIds) {
|
|
502
|
+
for (const entryId of result.entryNodeIds) {
|
|
503
|
+
edges.push({
|
|
504
|
+
id: `e_${prevId}_${entryId}`,
|
|
505
|
+
source: prevId,
|
|
506
|
+
target: entryId,
|
|
507
|
+
type: 'default',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
exitNodeIds = result.exitNodeIds;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (stmt.type === 'ExpressionStatement') {
|
|
517
|
+
const result = analyzeExpression(stmt.expression, stepDeclarations, context, functionMap, variableMap);
|
|
518
|
+
nodes.push(...result.nodes);
|
|
519
|
+
edges.push(...result.edges);
|
|
520
|
+
entryNodeIds = result.entryNodeIds;
|
|
521
|
+
exitNodeIds = result.exitNodeIds;
|
|
522
|
+
}
|
|
523
|
+
if (stmt.type === 'IfStatement') {
|
|
524
|
+
const savedConditional = context.inConditional;
|
|
525
|
+
const conditionalId = `cond_${context.conditionalCounter++}`;
|
|
526
|
+
context.inConditional = conditionalId;
|
|
527
|
+
// Analyze the "then" branch first to check if it has any workflow-relevant nodes
|
|
528
|
+
let thenResult;
|
|
529
|
+
if (stmt.consequent.type === 'BlockStatement') {
|
|
530
|
+
thenResult = analyzeBlock(stmt.consequent.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Handle single-statement consequent (no braces)
|
|
534
|
+
thenResult = analyzeStatement(stmt.consequent, stepDeclarations, context, functionMap, variableMap);
|
|
535
|
+
}
|
|
536
|
+
// Analyze the "else" branch if it exists
|
|
537
|
+
let elseResult = null;
|
|
538
|
+
if (stmt.alternate) {
|
|
539
|
+
if (stmt.alternate.type === 'BlockStatement') {
|
|
540
|
+
elseResult = analyzeBlock(stmt.alternate.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// Handle single-statement alternate (no braces) or else-if
|
|
544
|
+
elseResult = analyzeStatement(stmt.alternate, stepDeclarations, context, functionMap, variableMap);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Only create conditional node if at least one branch has workflow-relevant nodes.
|
|
548
|
+
// This avoids creating nodes for runtime assertions like `if (!ctx) { throw ... }`
|
|
549
|
+
const thenHasNodes = thenResult.nodes.length > 0;
|
|
550
|
+
const elseHasNodes = elseResult ? elseResult.nodes.length > 0 : false;
|
|
551
|
+
if (thenHasNodes || elseHasNodes) {
|
|
552
|
+
// Create the conditional decision node
|
|
553
|
+
const conditionText = getConditionText(stmt.test);
|
|
554
|
+
const condNodeId = `${conditionalId}_node`;
|
|
555
|
+
const condMetadata = {};
|
|
556
|
+
if (context.inLoop) {
|
|
557
|
+
condMetadata.loopId = context.inLoop;
|
|
558
|
+
}
|
|
559
|
+
const condNode = {
|
|
560
|
+
id: condNodeId,
|
|
561
|
+
type: 'conditional',
|
|
562
|
+
data: {
|
|
563
|
+
label: conditionText,
|
|
564
|
+
nodeKind: 'conditional',
|
|
565
|
+
},
|
|
566
|
+
metadata: Object.keys(condMetadata).length > 0 ? condMetadata : undefined,
|
|
567
|
+
};
|
|
568
|
+
nodes.push(condNode);
|
|
569
|
+
// The conditional node is the entry point
|
|
570
|
+
entryNodeIds.push(condNodeId);
|
|
571
|
+
for (const node of thenResult.nodes) {
|
|
572
|
+
if (!node.metadata)
|
|
573
|
+
node.metadata = {};
|
|
574
|
+
node.metadata.conditionalId = conditionalId;
|
|
575
|
+
node.metadata.conditionalBranch = 'Then';
|
|
576
|
+
}
|
|
577
|
+
nodes.push(...thenResult.nodes);
|
|
578
|
+
edges.push(...thenResult.edges);
|
|
579
|
+
// Create edge from conditional node to "then" branch with "true" label
|
|
580
|
+
for (const thenEntryId of thenResult.entryNodeIds) {
|
|
581
|
+
edges.push({
|
|
582
|
+
id: `e_${condNodeId}_${thenEntryId}_true`,
|
|
583
|
+
source: condNodeId,
|
|
584
|
+
target: thenEntryId,
|
|
585
|
+
type: 'conditional',
|
|
586
|
+
label: 'true',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
exitNodeIds.push(...thenResult.exitNodeIds);
|
|
590
|
+
if (elseResult) {
|
|
591
|
+
for (const node of elseResult.nodes) {
|
|
592
|
+
if (!node.metadata)
|
|
593
|
+
node.metadata = {};
|
|
594
|
+
node.metadata.conditionalId = conditionalId;
|
|
595
|
+
node.metadata.conditionalBranch = 'Else';
|
|
596
|
+
}
|
|
597
|
+
nodes.push(...elseResult.nodes);
|
|
598
|
+
edges.push(...elseResult.edges);
|
|
599
|
+
// Create edge from conditional node to "else" branch with "false" label
|
|
600
|
+
for (const elseEntryId of elseResult.entryNodeIds) {
|
|
601
|
+
edges.push({
|
|
602
|
+
id: `e_${condNodeId}_${elseEntryId}_false`,
|
|
603
|
+
source: condNodeId,
|
|
604
|
+
target: elseEntryId,
|
|
605
|
+
type: 'conditional',
|
|
606
|
+
label: 'false',
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
exitNodeIds.push(...elseResult.exitNodeIds);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Note: When there's no else branch, we don't add the conditional node as an exit.
|
|
613
|
+
// The then-branch exits are the only exits. This means the graph shows the "true" path;
|
|
614
|
+
// the "false" case (when condition is false and there's no else) implicitly means
|
|
615
|
+
// execution continues with no steps from this if statement.
|
|
616
|
+
//
|
|
617
|
+
// When both branches have no workflow-relevant nodes (e.g., runtime assertions like
|
|
618
|
+
// `if (!ctx) { throw ... }`), we skip creating the conditional node entirely.
|
|
619
|
+
context.inConditional = savedConditional;
|
|
620
|
+
}
|
|
621
|
+
if (stmt.type === 'WhileStatement' || stmt.type === 'ForStatement') {
|
|
622
|
+
const body = stmt.type === 'WhileStatement' ? stmt.body : stmt.body;
|
|
623
|
+
// Only treat as a loop if the body contains await expressions
|
|
624
|
+
// Otherwise it's likely a "collect promises" pattern (for parallel execution)
|
|
625
|
+
const hasAwait = containsAwaitExpression(body);
|
|
626
|
+
const loopId = hasAwait ? `loop_${context.loopCounter++}` : undefined;
|
|
627
|
+
const savedLoop = context.inLoop;
|
|
628
|
+
if (loopId) {
|
|
629
|
+
context.inLoop = loopId;
|
|
630
|
+
}
|
|
631
|
+
if (body.type === 'BlockStatement') {
|
|
632
|
+
const loopResult = analyzeBlock(body.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
633
|
+
// Only add loop metadata if this is truly a looping pattern
|
|
634
|
+
if (loopId) {
|
|
635
|
+
for (const node of loopResult.nodes) {
|
|
636
|
+
if (!node.metadata)
|
|
637
|
+
node.metadata = {};
|
|
638
|
+
node.metadata.loopId = loopId;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
nodes.push(...loopResult.nodes);
|
|
642
|
+
edges.push(...loopResult.edges);
|
|
643
|
+
entryNodeIds = loopResult.entryNodeIds;
|
|
644
|
+
exitNodeIds = loopResult.exitNodeIds;
|
|
645
|
+
// Only create loop-back edges if this is truly a looping pattern
|
|
646
|
+
if (loopId) {
|
|
647
|
+
for (const exitId of loopResult.exitNodeIds) {
|
|
648
|
+
for (const entryId of loopResult.entryNodeIds) {
|
|
649
|
+
edges.push({
|
|
650
|
+
id: `e_${exitId}_back_${entryId}`,
|
|
651
|
+
source: exitId,
|
|
652
|
+
target: entryId,
|
|
653
|
+
type: 'loop',
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
// Handle single-statement body (no braces)
|
|
661
|
+
const loopResult = analyzeStatement(body, stepDeclarations, context, functionMap, variableMap);
|
|
662
|
+
// Only add loop metadata if this is truly a looping pattern
|
|
663
|
+
if (loopId) {
|
|
664
|
+
for (const node of loopResult.nodes) {
|
|
665
|
+
if (!node.metadata)
|
|
666
|
+
node.metadata = {};
|
|
667
|
+
node.metadata.loopId = loopId;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
nodes.push(...loopResult.nodes);
|
|
671
|
+
edges.push(...loopResult.edges);
|
|
672
|
+
entryNodeIds = loopResult.entryNodeIds;
|
|
673
|
+
exitNodeIds = loopResult.exitNodeIds;
|
|
674
|
+
// Only create loop-back edges if this is truly a looping pattern
|
|
675
|
+
if (loopId) {
|
|
676
|
+
for (const exitId of loopResult.exitNodeIds) {
|
|
677
|
+
for (const entryId of loopResult.entryNodeIds) {
|
|
678
|
+
edges.push({
|
|
679
|
+
id: `e_${exitId}_back_${entryId}`,
|
|
680
|
+
source: exitId,
|
|
681
|
+
target: entryId,
|
|
682
|
+
type: 'loop',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
context.inLoop = savedLoop;
|
|
689
|
+
}
|
|
690
|
+
if (stmt.type === 'ForOfStatement') {
|
|
691
|
+
const loopId = `loop_${context.loopCounter++}`;
|
|
692
|
+
const savedLoop = context.inLoop;
|
|
693
|
+
context.inLoop = loopId;
|
|
694
|
+
const isAwait = stmt.isAwait || stmt.await;
|
|
695
|
+
const body = stmt.body;
|
|
696
|
+
if (body.type === 'BlockStatement') {
|
|
697
|
+
const loopResult = analyzeBlock(body.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
698
|
+
for (const node of loopResult.nodes) {
|
|
699
|
+
if (!node.metadata)
|
|
700
|
+
node.metadata = {};
|
|
701
|
+
node.metadata.loopId = loopId;
|
|
702
|
+
node.metadata.loopIsAwait = isAwait;
|
|
703
|
+
}
|
|
704
|
+
nodes.push(...loopResult.nodes);
|
|
705
|
+
edges.push(...loopResult.edges);
|
|
706
|
+
entryNodeIds = loopResult.entryNodeIds;
|
|
707
|
+
exitNodeIds = loopResult.exitNodeIds;
|
|
708
|
+
for (const exitId of loopResult.exitNodeIds) {
|
|
709
|
+
for (const entryId of loopResult.entryNodeIds) {
|
|
710
|
+
edges.push({
|
|
711
|
+
id: `e_${exitId}_back_${entryId}`,
|
|
712
|
+
source: exitId,
|
|
713
|
+
target: entryId,
|
|
714
|
+
type: 'loop',
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// Handle single-statement body (no braces)
|
|
721
|
+
const loopResult = analyzeStatement(body, stepDeclarations, context, functionMap, variableMap);
|
|
722
|
+
for (const node of loopResult.nodes) {
|
|
723
|
+
if (!node.metadata)
|
|
724
|
+
node.metadata = {};
|
|
725
|
+
node.metadata.loopId = loopId;
|
|
726
|
+
node.metadata.loopIsAwait = isAwait;
|
|
727
|
+
}
|
|
728
|
+
nodes.push(...loopResult.nodes);
|
|
729
|
+
edges.push(...loopResult.edges);
|
|
730
|
+
entryNodeIds = loopResult.entryNodeIds;
|
|
731
|
+
exitNodeIds = loopResult.exitNodeIds;
|
|
732
|
+
for (const exitId of loopResult.exitNodeIds) {
|
|
733
|
+
for (const entryId of loopResult.entryNodeIds) {
|
|
734
|
+
edges.push({
|
|
735
|
+
id: `e_${exitId}_back_${entryId}`,
|
|
736
|
+
source: exitId,
|
|
737
|
+
target: entryId,
|
|
738
|
+
type: 'loop',
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
context.inLoop = savedLoop;
|
|
744
|
+
}
|
|
745
|
+
// Handle plain BlockStatement (bare blocks like { ... })
|
|
746
|
+
if (stmt.type === 'BlockStatement') {
|
|
747
|
+
const blockResult = analyzeBlock(stmt.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
748
|
+
nodes.push(...blockResult.nodes);
|
|
749
|
+
edges.push(...blockResult.edges);
|
|
750
|
+
entryNodeIds = blockResult.entryNodeIds;
|
|
751
|
+
exitNodeIds = blockResult.exitNodeIds;
|
|
752
|
+
}
|
|
753
|
+
if (stmt.type === 'ReturnStatement' && stmt.argument) {
|
|
754
|
+
const result = analyzeExpression(stmt.argument, stepDeclarations, context, functionMap, variableMap);
|
|
755
|
+
nodes.push(...result.nodes);
|
|
756
|
+
edges.push(...result.edges);
|
|
757
|
+
entryNodeIds = result.entryNodeIds;
|
|
758
|
+
exitNodeIds = result.exitNodeIds;
|
|
759
|
+
}
|
|
760
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Analyze a block of statements with proper sequential chaining
|
|
764
|
+
*/
|
|
765
|
+
function analyzeBlock(stmts, stepDeclarations, context, functionMap, variableMap) {
|
|
766
|
+
const nodes = [];
|
|
767
|
+
const edges = [];
|
|
768
|
+
let entryNodeIds = [];
|
|
769
|
+
let currentExitIds = [];
|
|
770
|
+
for (const stmt of stmts) {
|
|
771
|
+
const result = analyzeStatement(stmt, stepDeclarations, context, functionMap, variableMap);
|
|
772
|
+
if (result.nodes.length === 0)
|
|
773
|
+
continue;
|
|
774
|
+
nodes.push(...result.nodes);
|
|
775
|
+
edges.push(...result.edges);
|
|
776
|
+
if (entryNodeIds.length === 0 && result.entryNodeIds.length > 0) {
|
|
777
|
+
entryNodeIds = result.entryNodeIds;
|
|
778
|
+
}
|
|
779
|
+
if (currentExitIds.length > 0 && result.entryNodeIds.length > 0) {
|
|
780
|
+
for (const prevId of currentExitIds) {
|
|
781
|
+
for (const entryId of result.entryNodeIds) {
|
|
782
|
+
const targetNode = result.nodes.find((n) => n.id === entryId);
|
|
783
|
+
const edgeType = targetNode?.metadata?.parallelGroupId
|
|
784
|
+
? 'parallel'
|
|
785
|
+
: 'default';
|
|
786
|
+
edges.push({
|
|
787
|
+
id: `e_${prevId}_${entryId}`,
|
|
788
|
+
source: prevId,
|
|
789
|
+
target: entryId,
|
|
790
|
+
type: edgeType,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (result.exitNodeIds.length > 0) {
|
|
796
|
+
currentExitIds = result.exitNodeIds;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return { nodes, edges, entryNodeIds, exitNodeIds: currentExitIds };
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Analyze an expression and extract step calls
|
|
803
|
+
*/
|
|
804
|
+
function analyzeExpression(expr, stepDeclarations, context, functionMap, variableMap, visitedFunctions = new Set()) {
|
|
805
|
+
const nodes = [];
|
|
806
|
+
const edges = [];
|
|
807
|
+
const entryNodeIds = [];
|
|
808
|
+
const exitNodeIds = [];
|
|
809
|
+
if (expr.type === 'AwaitExpression') {
|
|
810
|
+
const awaitedExpr = expr.argument;
|
|
811
|
+
if (awaitedExpr.type === 'CallExpression') {
|
|
812
|
+
const callExpr = awaitedExpr;
|
|
813
|
+
// Check for Promise.all/race/allSettled/any
|
|
814
|
+
if (callExpr.callee.type === 'MemberExpression') {
|
|
815
|
+
const member = callExpr.callee;
|
|
816
|
+
if (member.object.type === 'Identifier' &&
|
|
817
|
+
member.object.value === 'Promise' &&
|
|
818
|
+
member.property.type === 'Identifier') {
|
|
819
|
+
const method = member.property.value;
|
|
820
|
+
if (['all', 'race', 'allSettled', 'any'].includes(method)) {
|
|
821
|
+
const parallelId = `parallel_${context.parallelCounter++}`;
|
|
822
|
+
if (callExpr.arguments.length > 0) {
|
|
823
|
+
const arg = callExpr.arguments[0].expression;
|
|
824
|
+
if (arg.type === 'ArrayExpression') {
|
|
825
|
+
for (const element of arg.elements) {
|
|
826
|
+
if (element?.expression) {
|
|
827
|
+
const elemResult = analyzeExpression(element.expression, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
828
|
+
for (const node of elemResult.nodes) {
|
|
829
|
+
if (!node.metadata)
|
|
830
|
+
node.metadata = {};
|
|
831
|
+
node.metadata.parallelGroupId = parallelId;
|
|
832
|
+
node.metadata.parallelMethod = method;
|
|
833
|
+
if (context.inLoop) {
|
|
834
|
+
node.metadata.loopId = context.inLoop;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
nodes.push(...elemResult.nodes);
|
|
838
|
+
edges.push(...elemResult.edges);
|
|
839
|
+
entryNodeIds.push(...elemResult.entryNodeIds);
|
|
840
|
+
exitNodeIds.push(...elemResult.exitNodeIds);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (arg.type === 'Identifier' &&
|
|
845
|
+
context.promiseArrays.has(arg.value)) {
|
|
846
|
+
// Handle Promise.all(variableName) where variable was built via push()
|
|
847
|
+
const arrayName = arg.value;
|
|
848
|
+
const trackedNodes = context.promiseArrays.get(arrayName);
|
|
849
|
+
// Apply parallelGroupId to all nodes that were pushed to this array
|
|
850
|
+
if (trackedNodes && trackedNodes.length > 0) {
|
|
851
|
+
for (const trackedNode of trackedNodes) {
|
|
852
|
+
if (!trackedNode.metadata)
|
|
853
|
+
trackedNode.metadata = {};
|
|
854
|
+
trackedNode.metadata.parallelGroupId = parallelId;
|
|
855
|
+
trackedNode.metadata.parallelMethod = method;
|
|
856
|
+
if (context.inLoop) {
|
|
857
|
+
trackedNode.metadata.loopId = context.inLoop;
|
|
858
|
+
}
|
|
859
|
+
// Return tracked node IDs for proper edge connections
|
|
860
|
+
entryNodeIds.push(trackedNode.id);
|
|
861
|
+
exitNodeIds.push(trackedNode.id);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
// Handle non-array arguments like array.map(stepFn)
|
|
867
|
+
const argResult = analyzeExpression(arg, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
868
|
+
for (const node of argResult.nodes) {
|
|
869
|
+
if (!node.metadata)
|
|
870
|
+
node.metadata = {};
|
|
871
|
+
node.metadata.parallelGroupId = parallelId;
|
|
872
|
+
node.metadata.parallelMethod = method;
|
|
873
|
+
if (context.inLoop) {
|
|
874
|
+
node.metadata.loopId = context.inLoop;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
nodes.push(...argResult.nodes);
|
|
878
|
+
edges.push(...argResult.edges);
|
|
879
|
+
entryNodeIds.push(...argResult.entryNodeIds);
|
|
880
|
+
exitNodeIds.push(...argResult.exitNodeIds);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Regular call - check if it's a step, workflow primitive, or helper function
|
|
888
|
+
if (callExpr.callee.type === 'Identifier') {
|
|
889
|
+
const funcName = callExpr.callee.value;
|
|
890
|
+
const stepInfo = stepDeclarations.get(funcName);
|
|
891
|
+
if (stepInfo) {
|
|
892
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
893
|
+
const metadata = {};
|
|
894
|
+
if (context.inLoop) {
|
|
895
|
+
metadata.loopId = context.inLoop;
|
|
896
|
+
}
|
|
897
|
+
if (context.inConditional) {
|
|
898
|
+
metadata.conditionalId = context.inConditional;
|
|
899
|
+
}
|
|
900
|
+
const node = {
|
|
901
|
+
id: nodeId,
|
|
902
|
+
type: 'step',
|
|
903
|
+
data: {
|
|
904
|
+
label: getOriginalStepName(stepInfo.stepId, funcName),
|
|
905
|
+
nodeKind: 'step',
|
|
906
|
+
stepId: stepInfo.stepId,
|
|
907
|
+
},
|
|
908
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
909
|
+
};
|
|
910
|
+
nodes.push(node);
|
|
911
|
+
entryNodeIds.push(nodeId);
|
|
912
|
+
exitNodeIds.push(nodeId);
|
|
913
|
+
}
|
|
914
|
+
else if (WORKFLOW_PRIMITIVES.has(funcName)) {
|
|
915
|
+
// Handle workflow primitives like sleep
|
|
916
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
917
|
+
const metadata = {};
|
|
918
|
+
if (context.inLoop) {
|
|
919
|
+
metadata.loopId = context.inLoop;
|
|
920
|
+
}
|
|
921
|
+
if (context.inConditional) {
|
|
922
|
+
metadata.conditionalId = context.inConditional;
|
|
923
|
+
}
|
|
924
|
+
const node = {
|
|
925
|
+
id: nodeId,
|
|
926
|
+
type: 'primitive',
|
|
927
|
+
data: {
|
|
928
|
+
label: funcName,
|
|
929
|
+
nodeKind: 'primitive',
|
|
930
|
+
},
|
|
931
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
932
|
+
};
|
|
933
|
+
nodes.push(node);
|
|
934
|
+
entryNodeIds.push(nodeId);
|
|
935
|
+
exitNodeIds.push(nodeId);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
const transitiveResult = analyzeTransitiveCall(funcName, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
939
|
+
nodes.push(...transitiveResult.nodes);
|
|
940
|
+
edges.push(...transitiveResult.edges);
|
|
941
|
+
entryNodeIds.push(...transitiveResult.entryNodeIds);
|
|
942
|
+
exitNodeIds.push(...transitiveResult.exitNodeIds);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Also analyze the arguments of awaited calls for step references in objects
|
|
946
|
+
for (const arg of callExpr.arguments) {
|
|
947
|
+
if (arg.expression?.type === 'ObjectExpression') {
|
|
948
|
+
const refResult = analyzeObjectForStepReferences(arg.expression, stepDeclarations, context, '');
|
|
949
|
+
nodes.push(...refResult.nodes);
|
|
950
|
+
edges.push(...refResult.edges);
|
|
951
|
+
entryNodeIds.push(...refResult.entryNodeIds);
|
|
952
|
+
exitNodeIds.push(...refResult.exitNodeIds);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Handle await on a webhook/hook variable: await webhook
|
|
957
|
+
if (awaitedExpr.type === 'Identifier') {
|
|
958
|
+
const varName = awaitedExpr.value;
|
|
959
|
+
if (context.webhookVariables.has(varName)) {
|
|
960
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
961
|
+
const metadata = {};
|
|
962
|
+
if (context.inLoop) {
|
|
963
|
+
metadata.loopId = context.inLoop;
|
|
964
|
+
}
|
|
965
|
+
if (context.inConditional) {
|
|
966
|
+
metadata.conditionalId = context.inConditional;
|
|
967
|
+
}
|
|
968
|
+
const node = {
|
|
969
|
+
id: nodeId,
|
|
970
|
+
type: 'primitive',
|
|
971
|
+
data: {
|
|
972
|
+
label: 'awaitWebhook',
|
|
973
|
+
nodeKind: 'primitive',
|
|
974
|
+
},
|
|
975
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
976
|
+
};
|
|
977
|
+
nodes.push(node);
|
|
978
|
+
entryNodeIds.push(nodeId);
|
|
979
|
+
exitNodeIds.push(nodeId);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Non-awaited call expression
|
|
984
|
+
if (expr.type === 'CallExpression') {
|
|
985
|
+
const callExpr = expr;
|
|
986
|
+
if (callExpr.callee.type === 'Identifier') {
|
|
987
|
+
const funcName = callExpr.callee.value;
|
|
988
|
+
const stepInfo = stepDeclarations.get(funcName);
|
|
989
|
+
if (stepInfo) {
|
|
990
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
991
|
+
const metadata = {};
|
|
992
|
+
if (context.inLoop) {
|
|
993
|
+
metadata.loopId = context.inLoop;
|
|
994
|
+
}
|
|
995
|
+
if (context.inConditional) {
|
|
996
|
+
metadata.conditionalId = context.inConditional;
|
|
997
|
+
}
|
|
998
|
+
const node = {
|
|
999
|
+
id: nodeId,
|
|
1000
|
+
type: 'step',
|
|
1001
|
+
data: {
|
|
1002
|
+
label: getOriginalStepName(stepInfo.stepId, funcName),
|
|
1003
|
+
nodeKind: 'step',
|
|
1004
|
+
stepId: stepInfo.stepId,
|
|
1005
|
+
},
|
|
1006
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1007
|
+
};
|
|
1008
|
+
nodes.push(node);
|
|
1009
|
+
entryNodeIds.push(nodeId);
|
|
1010
|
+
exitNodeIds.push(nodeId);
|
|
1011
|
+
}
|
|
1012
|
+
else if (WORKFLOW_PRIMITIVES.has(funcName)) {
|
|
1013
|
+
// Handle non-awaited workflow primitives like createHook, createWebhook
|
|
1014
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1015
|
+
const metadata = {};
|
|
1016
|
+
if (context.inLoop) {
|
|
1017
|
+
metadata.loopId = context.inLoop;
|
|
1018
|
+
}
|
|
1019
|
+
if (context.inConditional) {
|
|
1020
|
+
metadata.conditionalId = context.inConditional;
|
|
1021
|
+
}
|
|
1022
|
+
const node = {
|
|
1023
|
+
id: nodeId,
|
|
1024
|
+
type: 'primitive',
|
|
1025
|
+
data: {
|
|
1026
|
+
label: funcName,
|
|
1027
|
+
nodeKind: 'primitive',
|
|
1028
|
+
},
|
|
1029
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1030
|
+
};
|
|
1031
|
+
nodes.push(node);
|
|
1032
|
+
entryNodeIds.push(nodeId);
|
|
1033
|
+
exitNodeIds.push(nodeId);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
const transitiveResult = analyzeTransitiveCall(funcName, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
1037
|
+
nodes.push(...transitiveResult.nodes);
|
|
1038
|
+
edges.push(...transitiveResult.edges);
|
|
1039
|
+
entryNodeIds.push(...transitiveResult.entryNodeIds);
|
|
1040
|
+
exitNodeIds.push(...transitiveResult.exitNodeIds);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
// Check for step references in object literals
|
|
1045
|
+
if (expr.type === 'ObjectExpression') {
|
|
1046
|
+
const refResult = analyzeObjectForStepReferences(expr, stepDeclarations, context, '');
|
|
1047
|
+
nodes.push(...refResult.nodes);
|
|
1048
|
+
edges.push(...refResult.edges);
|
|
1049
|
+
entryNodeIds.push(...refResult.entryNodeIds);
|
|
1050
|
+
exitNodeIds.push(...refResult.exitNodeIds);
|
|
1051
|
+
}
|
|
1052
|
+
// Check for step references and step calls in function call arguments
|
|
1053
|
+
// Skip for array methods (map, forEach, etc.) which have a specialized handler below
|
|
1054
|
+
if (expr.type === 'CallExpression') {
|
|
1055
|
+
const callExpr = expr;
|
|
1056
|
+
// Check if this is an array method call - if so, skip the generic handler
|
|
1057
|
+
// and let the specialized handler at the end of this function handle it
|
|
1058
|
+
const isArrayMethodCall = callExpr.callee.type === 'MemberExpression' &&
|
|
1059
|
+
callExpr.callee.property.type === 'Identifier' &&
|
|
1060
|
+
['map', 'forEach', 'filter', 'find', 'some', 'every', 'flatMap'].includes(callExpr.callee.property.value);
|
|
1061
|
+
// Check if this is a .push() call on a tracked promise array
|
|
1062
|
+
// Pattern: promises.push(stepCall())
|
|
1063
|
+
let pushArrayName = null;
|
|
1064
|
+
if (callExpr.callee.type === 'MemberExpression' &&
|
|
1065
|
+
callExpr.callee.object.type === 'Identifier' &&
|
|
1066
|
+
callExpr.callee.property.type === 'Identifier' &&
|
|
1067
|
+
callExpr.callee.property.value ===
|
|
1068
|
+
'push') {
|
|
1069
|
+
const objName = callExpr.callee.object.value;
|
|
1070
|
+
if (context.promiseArrays.has(objName)) {
|
|
1071
|
+
pushArrayName = objName;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const arg of callExpr.arguments) {
|
|
1075
|
+
if (arg.expression) {
|
|
1076
|
+
// For array method calls, skip step identifier detection here
|
|
1077
|
+
// since we have a specialized handler for those
|
|
1078
|
+
if (arg.expression.type === 'Identifier' && !isArrayMethodCall) {
|
|
1079
|
+
const argName = arg.expression.value;
|
|
1080
|
+
const stepInfo = stepDeclarations.get(argName);
|
|
1081
|
+
if (stepInfo) {
|
|
1082
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1083
|
+
const node = {
|
|
1084
|
+
id: nodeId,
|
|
1085
|
+
type: 'step',
|
|
1086
|
+
data: {
|
|
1087
|
+
label: `${getOriginalStepName(stepInfo.stepId, argName)} (ref)`,
|
|
1088
|
+
nodeKind: 'step',
|
|
1089
|
+
stepId: stepInfo.stepId,
|
|
1090
|
+
},
|
|
1091
|
+
metadata: {
|
|
1092
|
+
isStepReference: true,
|
|
1093
|
+
referenceContext: 'function argument',
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
nodes.push(node);
|
|
1097
|
+
entryNodeIds.push(nodeId);
|
|
1098
|
+
exitNodeIds.push(nodeId);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Handle step calls passed as arguments (e.g., promises.push(stepCall()))
|
|
1102
|
+
// Note: Don't add loopId here - these are non-awaited calls being collected
|
|
1103
|
+
// for parallel execution (like Promise.all), not truly looping calls
|
|
1104
|
+
if (arg.expression.type === 'CallExpression') {
|
|
1105
|
+
const argCallExpr = arg.expression;
|
|
1106
|
+
if (argCallExpr.callee.type === 'Identifier') {
|
|
1107
|
+
const funcName = argCallExpr.callee.value;
|
|
1108
|
+
const stepInfo = stepDeclarations.get(funcName);
|
|
1109
|
+
if (stepInfo) {
|
|
1110
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1111
|
+
const metadata = {};
|
|
1112
|
+
// Don't add loopId - this is a non-awaited call, likely being
|
|
1113
|
+
// collected for parallel execution (Promise.all pattern)
|
|
1114
|
+
if (context.inConditional) {
|
|
1115
|
+
metadata.conditionalId = context.inConditional;
|
|
1116
|
+
}
|
|
1117
|
+
const node = {
|
|
1118
|
+
id: nodeId,
|
|
1119
|
+
type: 'step',
|
|
1120
|
+
data: {
|
|
1121
|
+
label: getOriginalStepName(stepInfo.stepId, funcName),
|
|
1122
|
+
nodeKind: 'step',
|
|
1123
|
+
stepId: stepInfo.stepId,
|
|
1124
|
+
},
|
|
1125
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1126
|
+
};
|
|
1127
|
+
// If this is being pushed to a tracked promise array, store the node
|
|
1128
|
+
// so we can apply parallelGroupId when Promise.all is reached
|
|
1129
|
+
if (pushArrayName) {
|
|
1130
|
+
const trackedNodes = context.promiseArrays.get(pushArrayName);
|
|
1131
|
+
if (trackedNodes) {
|
|
1132
|
+
trackedNodes.push(node);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
nodes.push(node);
|
|
1136
|
+
entryNodeIds.push(nodeId);
|
|
1137
|
+
exitNodeIds.push(nodeId);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (arg.expression.type === 'ObjectExpression') {
|
|
1142
|
+
const refResult = analyzeObjectForStepReferences(arg.expression, stepDeclarations, context, '');
|
|
1143
|
+
nodes.push(...refResult.nodes);
|
|
1144
|
+
edges.push(...refResult.edges);
|
|
1145
|
+
entryNodeIds.push(...refResult.entryNodeIds);
|
|
1146
|
+
exitNodeIds.push(...refResult.exitNodeIds);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// Check for step references in 'new' expressions
|
|
1152
|
+
if (expr.type === 'NewExpression') {
|
|
1153
|
+
const newExpr = expr;
|
|
1154
|
+
// Check if this is a DurableAgent instantiation
|
|
1155
|
+
const isDurableAgent = newExpr.callee?.type === 'Identifier' &&
|
|
1156
|
+
newExpr.callee?.value === 'DurableAgent';
|
|
1157
|
+
if (isDurableAgent && newExpr.arguments?.length > 0) {
|
|
1158
|
+
// Create a node for the DurableAgent itself
|
|
1159
|
+
const agentNodeId = `node_${context.nodeCounter++}`;
|
|
1160
|
+
const agentNode = {
|
|
1161
|
+
id: agentNodeId,
|
|
1162
|
+
type: 'agent',
|
|
1163
|
+
data: {
|
|
1164
|
+
label: 'DurableAgent',
|
|
1165
|
+
nodeKind: 'agent',
|
|
1166
|
+
},
|
|
1167
|
+
metadata: {
|
|
1168
|
+
isStepReference: true,
|
|
1169
|
+
referenceContext: 'DurableAgent',
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
nodes.push(agentNode);
|
|
1173
|
+
entryNodeIds.push(agentNodeId);
|
|
1174
|
+
// Look for tools in the constructor options
|
|
1175
|
+
const optionsArg = newExpr.arguments[0]?.expression;
|
|
1176
|
+
if (optionsArg?.type === 'ObjectExpression') {
|
|
1177
|
+
const toolsResult = analyzeDurableAgentTools(optionsArg, stepDeclarations, context, agentNodeId, variableMap);
|
|
1178
|
+
nodes.push(...toolsResult.nodes);
|
|
1179
|
+
edges.push(...toolsResult.edges);
|
|
1180
|
+
// If we found tools, they are the exit nodes
|
|
1181
|
+
if (toolsResult.exitNodeIds.length > 0) {
|
|
1182
|
+
exitNodeIds.push(...toolsResult.exitNodeIds);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
exitNodeIds.push(agentNodeId);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
exitNodeIds.push(agentNodeId);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else if (newExpr.arguments) {
|
|
1193
|
+
for (const arg of newExpr.arguments) {
|
|
1194
|
+
if (arg.expression?.type === 'ObjectExpression') {
|
|
1195
|
+
const refResult = analyzeObjectForStepReferences(arg.expression, stepDeclarations, context, '');
|
|
1196
|
+
nodes.push(...refResult.nodes);
|
|
1197
|
+
edges.push(...refResult.edges);
|
|
1198
|
+
entryNodeIds.push(...refResult.entryNodeIds);
|
|
1199
|
+
exitNodeIds.push(...refResult.exitNodeIds);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Handle AssignmentExpression - analyze the right-hand side
|
|
1205
|
+
if (expr.type === 'AssignmentExpression') {
|
|
1206
|
+
const assignExpr = expr;
|
|
1207
|
+
if (assignExpr.right) {
|
|
1208
|
+
const rightResult = analyzeExpression(assignExpr.right, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
1209
|
+
nodes.push(...rightResult.nodes);
|
|
1210
|
+
edges.push(...rightResult.edges);
|
|
1211
|
+
entryNodeIds.push(...rightResult.entryNodeIds);
|
|
1212
|
+
exitNodeIds.push(...rightResult.exitNodeIds);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Handle MemberExpression calls like array.map(stepFn) where step is passed as callback
|
|
1216
|
+
if (expr.type === 'CallExpression') {
|
|
1217
|
+
const callExpr = expr;
|
|
1218
|
+
if (callExpr.callee.type === 'MemberExpression') {
|
|
1219
|
+
const member = callExpr.callee;
|
|
1220
|
+
// Check if this is a method call like .map(), .forEach(), .filter() etc.
|
|
1221
|
+
if (member.property.type === 'Identifier') {
|
|
1222
|
+
const methodName = member.property.value;
|
|
1223
|
+
if ([
|
|
1224
|
+
'map',
|
|
1225
|
+
'forEach',
|
|
1226
|
+
'filter',
|
|
1227
|
+
'find',
|
|
1228
|
+
'some',
|
|
1229
|
+
'every',
|
|
1230
|
+
'flatMap',
|
|
1231
|
+
].includes(methodName)) {
|
|
1232
|
+
// Check if any argument is a step function reference
|
|
1233
|
+
for (const arg of callExpr.arguments) {
|
|
1234
|
+
if (arg.expression?.type === 'Identifier') {
|
|
1235
|
+
const argName = arg.expression.value;
|
|
1236
|
+
const stepInfo = stepDeclarations.get(argName);
|
|
1237
|
+
if (stepInfo) {
|
|
1238
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1239
|
+
const metadata = {};
|
|
1240
|
+
if (context.inLoop) {
|
|
1241
|
+
metadata.loopId = context.inLoop;
|
|
1242
|
+
}
|
|
1243
|
+
if (context.inConditional) {
|
|
1244
|
+
metadata.conditionalId = context.inConditional;
|
|
1245
|
+
}
|
|
1246
|
+
const node = {
|
|
1247
|
+
id: nodeId,
|
|
1248
|
+
type: 'step',
|
|
1249
|
+
data: {
|
|
1250
|
+
label: getOriginalStepName(stepInfo.stepId, argName),
|
|
1251
|
+
nodeKind: 'step',
|
|
1252
|
+
stepId: stepInfo.stepId,
|
|
1253
|
+
},
|
|
1254
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1255
|
+
};
|
|
1256
|
+
nodes.push(node);
|
|
1257
|
+
entryNodeIds.push(nodeId);
|
|
1258
|
+
exitNodeIds.push(nodeId);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Analyze DurableAgent tools property to extract tool nodes
|
|
1270
|
+
*/
|
|
1271
|
+
function analyzeDurableAgentTools(optionsObj, stepDeclarations, context, agentNodeId, variableMap) {
|
|
1272
|
+
const nodes = [];
|
|
1273
|
+
const edges = [];
|
|
1274
|
+
const entryNodeIds = [];
|
|
1275
|
+
const exitNodeIds = [];
|
|
1276
|
+
if (!optionsObj.properties)
|
|
1277
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1278
|
+
// Helper function to extract tools from an ObjectExpression
|
|
1279
|
+
function extractToolsFromObject(toolsObj) {
|
|
1280
|
+
for (const toolProp of toolsObj.properties || []) {
|
|
1281
|
+
if (toolProp.type !== 'KeyValueProperty')
|
|
1282
|
+
continue;
|
|
1283
|
+
let toolName = '';
|
|
1284
|
+
if (toolProp.key.type === 'Identifier') {
|
|
1285
|
+
toolName = toolProp.key.value;
|
|
1286
|
+
}
|
|
1287
|
+
if (!toolName)
|
|
1288
|
+
continue;
|
|
1289
|
+
// Look for execute property in the tool definition
|
|
1290
|
+
if (toolProp.value.type === 'ObjectExpression') {
|
|
1291
|
+
for (const innerProp of toolProp.value.properties || []) {
|
|
1292
|
+
if (innerProp.type !== 'KeyValueProperty')
|
|
1293
|
+
continue;
|
|
1294
|
+
let innerKey = '';
|
|
1295
|
+
if (innerProp.key.type === 'Identifier') {
|
|
1296
|
+
innerKey = innerProp.key.value;
|
|
1297
|
+
}
|
|
1298
|
+
if (innerKey === 'execute' && innerProp.value.type === 'Identifier') {
|
|
1299
|
+
const stepName = innerProp.value.value;
|
|
1300
|
+
const stepInfo = stepDeclarations.get(stepName);
|
|
1301
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1302
|
+
const node = {
|
|
1303
|
+
id: nodeId,
|
|
1304
|
+
type: 'tool',
|
|
1305
|
+
data: {
|
|
1306
|
+
label: stepName,
|
|
1307
|
+
nodeKind: 'tool',
|
|
1308
|
+
stepId: stepInfo?.stepId,
|
|
1309
|
+
},
|
|
1310
|
+
metadata: {
|
|
1311
|
+
isTool: true,
|
|
1312
|
+
toolName: toolName,
|
|
1313
|
+
referenceContext: `tools.${toolName}.execute`,
|
|
1314
|
+
},
|
|
1315
|
+
};
|
|
1316
|
+
nodes.push(node);
|
|
1317
|
+
exitNodeIds.push(nodeId);
|
|
1318
|
+
// Connect agent to this tool with tool edge type
|
|
1319
|
+
edges.push({
|
|
1320
|
+
id: `e_${agentNodeId}_${nodeId}`,
|
|
1321
|
+
source: agentNodeId,
|
|
1322
|
+
target: nodeId,
|
|
1323
|
+
type: 'tool',
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Find the 'tools' property
|
|
1331
|
+
for (const prop of optionsObj.properties) {
|
|
1332
|
+
if (prop.type !== 'KeyValueProperty')
|
|
1333
|
+
continue;
|
|
1334
|
+
let keyName = '';
|
|
1335
|
+
if (prop.key.type === 'Identifier') {
|
|
1336
|
+
keyName = prop.key.value;
|
|
1337
|
+
}
|
|
1338
|
+
if (keyName !== 'tools')
|
|
1339
|
+
continue;
|
|
1340
|
+
// Handle inline tools object
|
|
1341
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
1342
|
+
extractToolsFromObject(prop.value);
|
|
1343
|
+
}
|
|
1344
|
+
// Handle tools as a variable reference - resolve it from variableMap
|
|
1345
|
+
if (prop.value.type === 'Identifier') {
|
|
1346
|
+
const toolsVarName = prop.value.value;
|
|
1347
|
+
// Try to resolve the variable from the variableMap (bundled code)
|
|
1348
|
+
const resolvedToolsObj = variableMap.get(toolsVarName);
|
|
1349
|
+
if (resolvedToolsObj && resolvedToolsObj.type === 'ObjectExpression') {
|
|
1350
|
+
// Successfully resolved - extract individual tools
|
|
1351
|
+
extractToolsFromObject(resolvedToolsObj);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
// Fallback: create a placeholder node if we can't resolve
|
|
1355
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1356
|
+
const node = {
|
|
1357
|
+
id: nodeId,
|
|
1358
|
+
type: 'tool',
|
|
1359
|
+
data: {
|
|
1360
|
+
label: `${toolsVarName}`,
|
|
1361
|
+
nodeKind: 'tool',
|
|
1362
|
+
},
|
|
1363
|
+
metadata: {
|
|
1364
|
+
isToolsCollection: true,
|
|
1365
|
+
toolsVariable: toolsVarName,
|
|
1366
|
+
referenceContext: `tools:${toolsVarName}`,
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
nodes.push(node);
|
|
1370
|
+
exitNodeIds.push(nodeId);
|
|
1371
|
+
// Connect agent to tools with tool edge type
|
|
1372
|
+
edges.push({
|
|
1373
|
+
id: `e_${agentNodeId}_${nodeId}`,
|
|
1374
|
+
source: agentNodeId,
|
|
1375
|
+
target: nodeId,
|
|
1376
|
+
type: 'tool',
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Analyze an object expression for step references
|
|
1385
|
+
*/
|
|
1386
|
+
function analyzeObjectForStepReferences(obj, stepDeclarations, context, path) {
|
|
1387
|
+
const nodes = [];
|
|
1388
|
+
const edges = [];
|
|
1389
|
+
const entryNodeIds = [];
|
|
1390
|
+
const exitNodeIds = [];
|
|
1391
|
+
if (!obj.properties)
|
|
1392
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1393
|
+
for (const prop of obj.properties) {
|
|
1394
|
+
if (prop.type !== 'KeyValueProperty')
|
|
1395
|
+
continue;
|
|
1396
|
+
let keyName = '';
|
|
1397
|
+
if (prop.key.type === 'Identifier') {
|
|
1398
|
+
keyName = prop.key.value;
|
|
1399
|
+
}
|
|
1400
|
+
else if (prop.key.type === 'StringLiteral') {
|
|
1401
|
+
keyName = prop.key.value;
|
|
1402
|
+
}
|
|
1403
|
+
const currentPath = path ? `${path}.${keyName}` : keyName;
|
|
1404
|
+
if (prop.value.type === 'Identifier') {
|
|
1405
|
+
const valueName = prop.value.value;
|
|
1406
|
+
const stepInfo = stepDeclarations.get(valueName);
|
|
1407
|
+
if (stepInfo) {
|
|
1408
|
+
const nodeId = `node_${context.nodeCounter++}`;
|
|
1409
|
+
const node = {
|
|
1410
|
+
id: nodeId,
|
|
1411
|
+
type: 'step',
|
|
1412
|
+
data: {
|
|
1413
|
+
label: `${getOriginalStepName(stepInfo.stepId, valueName)} (tool)`,
|
|
1414
|
+
nodeKind: 'step',
|
|
1415
|
+
stepId: stepInfo.stepId,
|
|
1416
|
+
},
|
|
1417
|
+
metadata: {
|
|
1418
|
+
isStepReference: true,
|
|
1419
|
+
referenceContext: currentPath,
|
|
1420
|
+
},
|
|
1421
|
+
};
|
|
1422
|
+
nodes.push(node);
|
|
1423
|
+
entryNodeIds.push(nodeId);
|
|
1424
|
+
exitNodeIds.push(nodeId);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
1428
|
+
const nestedResult = analyzeObjectForStepReferences(prop.value, stepDeclarations, context, currentPath);
|
|
1429
|
+
nodes.push(...nestedResult.nodes);
|
|
1430
|
+
edges.push(...nestedResult.edges);
|
|
1431
|
+
entryNodeIds.push(...nestedResult.entryNodeIds);
|
|
1432
|
+
exitNodeIds.push(...nestedResult.exitNodeIds);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Analyze a transitive function call to find step calls within helper functions
|
|
1439
|
+
*/
|
|
1440
|
+
function analyzeTransitiveCall(funcName, stepDeclarations, context, functionMap, variableMap, visitedFunctions) {
|
|
1441
|
+
const nodes = [];
|
|
1442
|
+
const edges = [];
|
|
1443
|
+
const entryNodeIds = [];
|
|
1444
|
+
const exitNodeIds = [];
|
|
1445
|
+
if (visitedFunctions.has(funcName)) {
|
|
1446
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1447
|
+
}
|
|
1448
|
+
const funcInfo = functionMap.get(funcName);
|
|
1449
|
+
if (!funcInfo || funcInfo.isStep) {
|
|
1450
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1451
|
+
}
|
|
1452
|
+
visitedFunctions.add(funcName);
|
|
1453
|
+
try {
|
|
1454
|
+
if (funcInfo.body) {
|
|
1455
|
+
if (funcInfo.body.type === 'BlockStatement') {
|
|
1456
|
+
const bodyResult = analyzeBlock(funcInfo.body.stmts, stepDeclarations, context, functionMap, variableMap);
|
|
1457
|
+
nodes.push(...bodyResult.nodes);
|
|
1458
|
+
edges.push(...bodyResult.edges);
|
|
1459
|
+
entryNodeIds.push(...bodyResult.entryNodeIds);
|
|
1460
|
+
exitNodeIds.push(...bodyResult.exitNodeIds);
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
const exprResult = analyzeExpression(funcInfo.body, stepDeclarations, context, functionMap, variableMap, visitedFunctions);
|
|
1464
|
+
nodes.push(...exprResult.nodes);
|
|
1465
|
+
edges.push(...exprResult.edges);
|
|
1466
|
+
entryNodeIds.push(...exprResult.entryNodeIds);
|
|
1467
|
+
exitNodeIds.push(...exprResult.exitNodeIds);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
finally {
|
|
1472
|
+
visitedFunctions.delete(funcName);
|
|
1473
|
+
}
|
|
1474
|
+
return { nodes, edges, entryNodeIds, exitNodeIds };
|
|
1475
|
+
}
|
|
1476
|
+
//# sourceMappingURL=workflows-extractor.js.map
|