@pikku/inspector 0.11.0 → 0.11.1
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/CHANGELOG.md +16 -1
- package/dist/add/add-channel.js +11 -10
- package/dist/add/add-file-with-factory.js +10 -10
- package/dist/add/add-functions.js +57 -43
- package/dist/add/add-http-route.js +5 -4
- package/dist/add/add-mcp-prompt.js +6 -5
- package/dist/add/add-mcp-resource.js +6 -5
- package/dist/add/add-mcp-tool.js +6 -5
- package/dist/add/add-middleware.js +1 -1
- package/dist/add/add-permission.js +1 -1
- package/dist/add/add-queue-worker.js +6 -5
- package/dist/add/add-schedule.js +5 -4
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +92 -66
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +10 -6
- package/dist/types.d.ts +21 -8
- package/dist/utils/extract-function-node.d.ts +10 -0
- package/dist/utils/extract-function-node.js +38 -0
- package/dist/utils/extract-node-value.d.ts +8 -0
- package/dist/utils/extract-node-value.js +24 -0
- package/dist/utils/extract-service-metadata.d.ts +19 -0
- package/dist/utils/extract-service-metadata.js +244 -0
- package/dist/utils/get-files-and-methods.d.ts +3 -3
- package/dist/utils/get-files-and-methods.js +3 -3
- package/dist/utils/get-property-value.d.ts +13 -6
- package/dist/utils/get-property-value.js +51 -43
- package/dist/utils/post-process.d.ts +9 -0
- package/dist/utils/post-process.js +30 -3
- package/dist/utils/serialize-inspector-state.d.ts +18 -5
- package/dist/utils/serialize-inspector-state.js +12 -10
- package/dist/utils/write-service-metadata.d.ts +13 -0
- package/dist/utils/write-service-metadata.js +37 -0
- package/dist/visit.js +2 -2
- package/dist/workflow/extract-simple-workflow.d.ts +15 -0
- package/dist/workflow/extract-simple-workflow.js +803 -0
- package/dist/workflow/patterns.d.ts +39 -0
- package/dist/workflow/patterns.js +138 -0
- package/dist/workflow/validation.d.ts +28 -0
- package/dist/workflow/validation.js +124 -0
- package/package.json +4 -4
- package/src/add/add-channel.ts +37 -17
- package/src/add/add-file-with-factory.ts +10 -10
- package/src/add/add-functions.ts +72 -56
- package/src/add/add-http-route.ts +10 -5
- package/src/add/add-mcp-prompt.ts +11 -7
- package/src/add/add-mcp-resource.ts +11 -7
- package/src/add/add-mcp-tool.ts +11 -7
- package/src/add/add-middleware.ts +1 -1
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-queue-worker.ts +11 -12
- package/src/add/add-schedule.ts +10 -5
- package/src/add/add-workflow.ts +120 -110
- package/src/error-codes.ts +1 -0
- package/src/index.ts +2 -0
- package/src/inspector.ts +16 -6
- package/src/types.ts +18 -8
- package/src/utils/extract-function-node.ts +58 -0
- package/src/utils/extract-node-value.ts +31 -0
- package/src/utils/extract-service-metadata.ts +353 -0
- package/src/utils/filter-inspector-state.test.ts +3 -3
- package/src/utils/filter-utils.test.ts +45 -51
- package/src/utils/get-files-and-methods.ts +11 -11
- package/src/utils/get-property-value.ts +60 -53
- package/src/utils/permissions.test.ts +3 -3
- package/src/utils/post-process.ts +56 -3
- package/src/utils/serialize-inspector-state.ts +28 -18
- package/src/utils/test-data/inspector-state.json +9 -9
- package/src/utils/write-service-metadata.ts +51 -0
- package/src/visit.ts +3 -3
- package/src/workflow/extract-simple-workflow.ts +1035 -0
- package/src/workflow/patterns.ts +182 -0
- package/src/workflow/validation.ts +153 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import {
|
|
3
|
+
WorkflowStepMeta,
|
|
4
|
+
RpcStepMeta,
|
|
5
|
+
BranchStepMeta,
|
|
6
|
+
ParallelGroupStepMeta,
|
|
7
|
+
FanoutStepMeta,
|
|
8
|
+
ReturnStepMeta,
|
|
9
|
+
InputSource,
|
|
10
|
+
OutputBinding,
|
|
11
|
+
} from '@pikku/core/workflow'
|
|
12
|
+
import {
|
|
13
|
+
extractStringLiteral,
|
|
14
|
+
extractNumberLiteral,
|
|
15
|
+
} from '../utils/extract-node-value.js'
|
|
16
|
+
import {
|
|
17
|
+
isWorkflowDoCall,
|
|
18
|
+
isWorkflowSleepCall,
|
|
19
|
+
isParallelFanout,
|
|
20
|
+
isParallelGroup,
|
|
21
|
+
isSequentialFanout,
|
|
22
|
+
extractForOfVariable,
|
|
23
|
+
isArrayType,
|
|
24
|
+
getSourceText,
|
|
25
|
+
} from './patterns.js'
|
|
26
|
+
import {
|
|
27
|
+
validateNoDisallowedPatterns,
|
|
28
|
+
validateAwaitedCalls,
|
|
29
|
+
formatValidationErrors,
|
|
30
|
+
ValidationError,
|
|
31
|
+
} from './validation.js'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extraction context to track state during AST traversal
|
|
35
|
+
*/
|
|
36
|
+
interface ExtractionContext {
|
|
37
|
+
checker: ts.TypeChecker
|
|
38
|
+
outputVars: Map<string, { type: ts.Type; node: ts.Node }>
|
|
39
|
+
arrayVars: Set<string>
|
|
40
|
+
conditionalVars: Set<string>
|
|
41
|
+
inputParamName: string
|
|
42
|
+
errors: ValidationError[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Result of simple workflow extraction
|
|
47
|
+
*/
|
|
48
|
+
export interface ExtractionResult {
|
|
49
|
+
status: 'ok' | 'error'
|
|
50
|
+
steps?: WorkflowStepMeta[]
|
|
51
|
+
reason?: string
|
|
52
|
+
simple?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract simple workflow metadata from a function declaration
|
|
57
|
+
*/
|
|
58
|
+
export function extractSimpleWorkflow(
|
|
59
|
+
funcNode: ts.Node,
|
|
60
|
+
checker: ts.TypeChecker
|
|
61
|
+
): ExtractionResult {
|
|
62
|
+
try {
|
|
63
|
+
// Find the async arrow function
|
|
64
|
+
const arrowFunc = findWorkflowFunction(funcNode)
|
|
65
|
+
if (!arrowFunc) {
|
|
66
|
+
return {
|
|
67
|
+
status: 'error',
|
|
68
|
+
reason: 'Could not find async arrow function in workflow definition',
|
|
69
|
+
simple: false,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extract input parameter name (second parameter)
|
|
74
|
+
const inputParamName = extractInputParamName(arrowFunc)
|
|
75
|
+
if (!inputParamName) {
|
|
76
|
+
return {
|
|
77
|
+
status: 'error',
|
|
78
|
+
reason: 'Could not determine input parameter name',
|
|
79
|
+
simple: false,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Initialize extraction context
|
|
84
|
+
const context: ExtractionContext = {
|
|
85
|
+
checker,
|
|
86
|
+
outputVars: new Map(),
|
|
87
|
+
arrayVars: new Set(),
|
|
88
|
+
conditionalVars: new Set(),
|
|
89
|
+
inputParamName,
|
|
90
|
+
errors: [],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate no disallowed patterns
|
|
94
|
+
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body)
|
|
95
|
+
if (patternErrors.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'error',
|
|
98
|
+
reason: formatValidationErrors(patternErrors),
|
|
99
|
+
simple: false,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate all workflow calls are awaited
|
|
104
|
+
const awaitErrors = validateAwaitedCalls(arrowFunc.body)
|
|
105
|
+
if (awaitErrors.length > 0) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'error',
|
|
108
|
+
reason: formatValidationErrors(awaitErrors),
|
|
109
|
+
simple: false,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract steps from function body
|
|
114
|
+
const steps = extractSteps(arrowFunc.body, context)
|
|
115
|
+
|
|
116
|
+
// Check for any accumulated errors
|
|
117
|
+
if (context.errors.length > 0) {
|
|
118
|
+
return {
|
|
119
|
+
status: 'error',
|
|
120
|
+
reason: formatValidationErrors(context.errors),
|
|
121
|
+
simple: false,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
status: 'ok',
|
|
127
|
+
steps,
|
|
128
|
+
simple: true,
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
status: 'error',
|
|
133
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
134
|
+
simple: false,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Find the workflow function (async arrow function)
|
|
141
|
+
*/
|
|
142
|
+
function findWorkflowFunction(node: ts.Node): ts.ArrowFunction | null {
|
|
143
|
+
// Handle pikkuSimpleWorkflowFunc(async () => {}) or pikkuWorkflowFunc(async () => {})
|
|
144
|
+
if (ts.isCallExpression(node)) {
|
|
145
|
+
const arg = node.arguments[0]
|
|
146
|
+
if (arg && ts.isArrowFunction(arg)) {
|
|
147
|
+
return arg
|
|
148
|
+
}
|
|
149
|
+
// Also check if first argument is an object with func property
|
|
150
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
151
|
+
for (const prop of arg.properties) {
|
|
152
|
+
if (
|
|
153
|
+
ts.isPropertyAssignment(prop) &&
|
|
154
|
+
ts.isIdentifier(prop.name) &&
|
|
155
|
+
prop.name.text === 'func'
|
|
156
|
+
) {
|
|
157
|
+
if (ts.isArrowFunction(prop.initializer)) {
|
|
158
|
+
return prop.initializer
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle pikkuSimpleWorkflowFunc({ func: async () => {} })
|
|
166
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
167
|
+
for (const prop of node.properties) {
|
|
168
|
+
if (
|
|
169
|
+
ts.isPropertyAssignment(prop) &&
|
|
170
|
+
ts.isIdentifier(prop.name) &&
|
|
171
|
+
prop.name.text === 'func'
|
|
172
|
+
) {
|
|
173
|
+
if (ts.isArrowFunction(prop.initializer)) {
|
|
174
|
+
return prop.initializer
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract the input parameter name from the arrow function
|
|
185
|
+
*/
|
|
186
|
+
function extractInputParamName(arrowFunc: ts.ArrowFunction): string | null {
|
|
187
|
+
if (arrowFunc.parameters.length < 2) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const secondParam = arrowFunc.parameters[1]
|
|
192
|
+
if (ts.isIdentifier(secondParam.name)) {
|
|
193
|
+
return secondParam.name.text
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract steps from the function body
|
|
201
|
+
*/
|
|
202
|
+
function extractSteps(
|
|
203
|
+
body: ts.Node,
|
|
204
|
+
context: ExtractionContext
|
|
205
|
+
): WorkflowStepMeta[] {
|
|
206
|
+
const steps: WorkflowStepMeta[] = []
|
|
207
|
+
|
|
208
|
+
if (!ts.isBlock(body)) {
|
|
209
|
+
return steps
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const statement of body.statements) {
|
|
213
|
+
const extracted = extractStep(statement, context)
|
|
214
|
+
if (extracted) {
|
|
215
|
+
steps.push(extracted)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return steps
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Extract a single step from a statement
|
|
224
|
+
*/
|
|
225
|
+
function extractStep(
|
|
226
|
+
statement: ts.Statement,
|
|
227
|
+
context: ExtractionContext
|
|
228
|
+
): WorkflowStepMeta | null {
|
|
229
|
+
// Variable declaration with workflow.do assignment
|
|
230
|
+
if (ts.isVariableStatement(statement)) {
|
|
231
|
+
return extractVariableDeclaration(statement, context)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Expression statement (await workflow.do without assignment)
|
|
235
|
+
if (ts.isExpressionStatement(statement)) {
|
|
236
|
+
return extractExpressionStatement(statement, context)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If statement (branch)
|
|
240
|
+
if (ts.isIfStatement(statement)) {
|
|
241
|
+
return extractBranch(statement, context)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// For-of statement (sequential fanout)
|
|
245
|
+
if (ts.isForOfStatement(statement)) {
|
|
246
|
+
return extractSequentialFanout(statement, context)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Return statement
|
|
250
|
+
if (ts.isReturnStatement(statement)) {
|
|
251
|
+
return extractReturn(statement, context)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract variable declaration (const x = await workflow.do(...))
|
|
259
|
+
*/
|
|
260
|
+
function extractVariableDeclaration(
|
|
261
|
+
statement: ts.VariableStatement,
|
|
262
|
+
context: ExtractionContext
|
|
263
|
+
): WorkflowStepMeta | null {
|
|
264
|
+
const declList = statement.declarationList
|
|
265
|
+
if (declList.declarations.length !== 1) {
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const decl = declList.declarations[0]
|
|
270
|
+
if (!ts.isIdentifier(decl.name)) {
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const varName = decl.name.text
|
|
275
|
+
const init = decl.initializer
|
|
276
|
+
|
|
277
|
+
if (!init) {
|
|
278
|
+
return null
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for await workflow.do(...)
|
|
282
|
+
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
283
|
+
const call = init.expression
|
|
284
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
285
|
+
const step = extractRpcStep(call, context, varName)
|
|
286
|
+
if (step) {
|
|
287
|
+
// Track output variable
|
|
288
|
+
const type = context.checker.getTypeAtLocation(decl)
|
|
289
|
+
context.outputVars.set(varName, { type, node: decl })
|
|
290
|
+
|
|
291
|
+
// Check if it's an array type
|
|
292
|
+
if (isArrayType(type, context.checker)) {
|
|
293
|
+
context.arrayVars.add(varName)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if it's a conditional variable (let x: T | undefined)
|
|
297
|
+
if (declList.flags & ts.NodeFlags.Let) {
|
|
298
|
+
const typeNode = decl.type
|
|
299
|
+
if (typeNode && ts.isUnionTypeNode(typeNode)) {
|
|
300
|
+
// Check if union includes undefined
|
|
301
|
+
const hasUndefined = typeNode.types.some(
|
|
302
|
+
(t) =>
|
|
303
|
+
(ts.isLiteralTypeNode(t) &&
|
|
304
|
+
t.literal.kind === ts.SyntaxKind.UndefinedKeyword) ||
|
|
305
|
+
t.kind === ts.SyntaxKind.UndefinedKeyword
|
|
306
|
+
)
|
|
307
|
+
if (hasUndefined) {
|
|
308
|
+
context.conditionalVars.add(varName)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return step
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract expression statement (await workflow.do(...) without assignment)
|
|
323
|
+
*/
|
|
324
|
+
function extractExpressionStatement(
|
|
325
|
+
statement: ts.ExpressionStatement,
|
|
326
|
+
context: ExtractionContext
|
|
327
|
+
): WorkflowStepMeta | null {
|
|
328
|
+
let expr = statement.expression
|
|
329
|
+
|
|
330
|
+
// Handle assignment: owner = await workflow.do(...)
|
|
331
|
+
let outputVar: string | undefined
|
|
332
|
+
if (
|
|
333
|
+
ts.isBinaryExpression(expr) &&
|
|
334
|
+
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
335
|
+
) {
|
|
336
|
+
// Extract variable name from left side
|
|
337
|
+
if (ts.isIdentifier(expr.left)) {
|
|
338
|
+
outputVar = expr.left.text
|
|
339
|
+
}
|
|
340
|
+
// Use right side as the expression to extract from
|
|
341
|
+
expr = expr.right
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// await workflow.do(...)
|
|
345
|
+
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
346
|
+
const call = expr.expression
|
|
347
|
+
|
|
348
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
349
|
+
const step = extractRpcStep(call, context, outputVar)
|
|
350
|
+
|
|
351
|
+
// Track output variable if this is an assignment
|
|
352
|
+
if (outputVar && step) {
|
|
353
|
+
const type = context.checker.getTypeAtLocation(expr)
|
|
354
|
+
context.outputVars.set(outputVar, { type, node: expr })
|
|
355
|
+
|
|
356
|
+
// Check if it's an array type
|
|
357
|
+
if (isArrayType(type, context.checker)) {
|
|
358
|
+
context.arrayVars.add(outputVar)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return step
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
366
|
+
return extractSleepStep(call, context)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check for parallel group or fanout
|
|
370
|
+
if (isParallelFanout(call)) {
|
|
371
|
+
return extractParallelFanout(call, context)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (isParallelGroup(call)) {
|
|
375
|
+
return extractParallelGroup(call, context)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Extract RPC step from workflow.do() call
|
|
384
|
+
*/
|
|
385
|
+
function extractRpcStep(
|
|
386
|
+
call: ts.CallExpression,
|
|
387
|
+
context: ExtractionContext,
|
|
388
|
+
outputVar?: string
|
|
389
|
+
): RpcStepMeta | null {
|
|
390
|
+
const args = call.arguments
|
|
391
|
+
|
|
392
|
+
if (args.length < 2) {
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const stepName = extractStringLiteral(args[0], context.checker)
|
|
398
|
+
const rpcName = extractStringLiteral(args[1], context.checker)
|
|
399
|
+
|
|
400
|
+
// Extract inputs from third argument
|
|
401
|
+
const inputs =
|
|
402
|
+
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
403
|
+
|
|
404
|
+
// Extract options from fourth argument
|
|
405
|
+
const options =
|
|
406
|
+
args.length >= 4 && ts.isObjectLiteralExpression(args[3])
|
|
407
|
+
? extractStepOptions(args[3], context)
|
|
408
|
+
: undefined
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
type: 'rpc',
|
|
412
|
+
stepName,
|
|
413
|
+
rpcName,
|
|
414
|
+
outputVar,
|
|
415
|
+
inputs,
|
|
416
|
+
options,
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
context.errors.push({
|
|
420
|
+
message: `Failed to extract RPC step: ${error instanceof Error ? error.message : String(error)}`,
|
|
421
|
+
node: call,
|
|
422
|
+
})
|
|
423
|
+
return null
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Extract step options from options object
|
|
429
|
+
*/
|
|
430
|
+
function extractStepOptions(
|
|
431
|
+
optionsNode: ts.ObjectLiteralExpression,
|
|
432
|
+
context: ExtractionContext
|
|
433
|
+
): RpcStepMeta['options'] {
|
|
434
|
+
const options: RpcStepMeta['options'] = {}
|
|
435
|
+
|
|
436
|
+
for (const prop of optionsNode.properties) {
|
|
437
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
438
|
+
const propName = prop.name.text
|
|
439
|
+
|
|
440
|
+
if (propName === 'retries') {
|
|
441
|
+
const retries = extractNumberLiteral(prop.initializer)
|
|
442
|
+
if (retries !== null) {
|
|
443
|
+
options.retries = retries
|
|
444
|
+
}
|
|
445
|
+
} else if (propName === 'retryDelay') {
|
|
446
|
+
try {
|
|
447
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
448
|
+
options.retryDelay = prop.initializer.text
|
|
449
|
+
} else {
|
|
450
|
+
const delay = extractNumberLiteral(prop.initializer)
|
|
451
|
+
if (delay !== null) {
|
|
452
|
+
options.retryDelay = delay
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// Ignore extraction errors for retryDelay
|
|
457
|
+
}
|
|
458
|
+
} else if (propName === 'description') {
|
|
459
|
+
try {
|
|
460
|
+
options.description = extractStringLiteral(
|
|
461
|
+
prop.initializer,
|
|
462
|
+
context.checker
|
|
463
|
+
)
|
|
464
|
+
} catch {
|
|
465
|
+
// Ignore extraction errors for description
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return Object.keys(options).length > 0 ? options : undefined
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Extract sleep step from workflow.sleep() call
|
|
476
|
+
*/
|
|
477
|
+
function extractSleepStep(
|
|
478
|
+
call: ts.CallExpression,
|
|
479
|
+
context: ExtractionContext
|
|
480
|
+
): WorkflowStepMeta | null {
|
|
481
|
+
const args = call.arguments
|
|
482
|
+
|
|
483
|
+
if (args.length < 2) {
|
|
484
|
+
return null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const stepName = extractStringLiteral(args[0], context.checker)
|
|
489
|
+
let duration: string | number
|
|
490
|
+
|
|
491
|
+
const numValue = extractNumberLiteral(args[1])
|
|
492
|
+
if (numValue !== null) {
|
|
493
|
+
duration = numValue
|
|
494
|
+
} else {
|
|
495
|
+
duration = extractStringLiteral(args[1], context.checker)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
type: 'sleep',
|
|
500
|
+
stepName,
|
|
501
|
+
duration,
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
context.errors.push({
|
|
505
|
+
message: `Failed to extract sleep step: ${error instanceof Error ? error.message : String(error)}`,
|
|
506
|
+
node: call,
|
|
507
|
+
})
|
|
508
|
+
return null
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Extract branch step from if statement
|
|
514
|
+
*/
|
|
515
|
+
function extractBranch(
|
|
516
|
+
statement: ts.IfStatement,
|
|
517
|
+
context: ExtractionContext
|
|
518
|
+
): BranchStepMeta | null {
|
|
519
|
+
const condition = getSourceText(statement.expression)
|
|
520
|
+
|
|
521
|
+
// Handle both block statements and single statements
|
|
522
|
+
const thenSteps = ts.isBlock(statement.thenStatement)
|
|
523
|
+
? extractSteps(statement.thenStatement, context)
|
|
524
|
+
: extractStepsFromStatement(statement.thenStatement, context)
|
|
525
|
+
|
|
526
|
+
const elseSteps = statement.elseStatement
|
|
527
|
+
? ts.isBlock(statement.elseStatement)
|
|
528
|
+
? extractSteps(statement.elseStatement, context)
|
|
529
|
+
: extractStepsFromStatement(statement.elseStatement, context)
|
|
530
|
+
: undefined
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
type: 'branch',
|
|
534
|
+
condition,
|
|
535
|
+
branches: {
|
|
536
|
+
then: thenSteps,
|
|
537
|
+
else: elseSteps,
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Extract steps from a single statement (non-block)
|
|
544
|
+
*/
|
|
545
|
+
function extractStepsFromStatement(
|
|
546
|
+
statement: ts.Statement,
|
|
547
|
+
context: ExtractionContext
|
|
548
|
+
): WorkflowStepMeta[] {
|
|
549
|
+
const step = extractStep(statement, context)
|
|
550
|
+
return step ? [step] : []
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Extract parallel fanout from Promise.all(array.map(...))
|
|
555
|
+
*/
|
|
556
|
+
function extractParallelFanout(
|
|
557
|
+
call: ts.CallExpression,
|
|
558
|
+
context: ExtractionContext
|
|
559
|
+
): FanoutStepMeta | null {
|
|
560
|
+
const mapCall = call.arguments[0]
|
|
561
|
+
if (!ts.isCallExpression(mapCall)) {
|
|
562
|
+
return null
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!ts.isPropertyAccessExpression(mapCall.expression)) {
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Extract source array
|
|
570
|
+
const sourceExpr = mapCall.expression.expression
|
|
571
|
+
let sourceVar: string | null = null
|
|
572
|
+
|
|
573
|
+
if (ts.isIdentifier(sourceExpr)) {
|
|
574
|
+
sourceVar = sourceExpr.text
|
|
575
|
+
} else if (
|
|
576
|
+
ts.isPropertyAccessExpression(sourceExpr) &&
|
|
577
|
+
ts.isIdentifier(sourceExpr.expression)
|
|
578
|
+
) {
|
|
579
|
+
sourceVar = sourceExpr.expression.text
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!sourceVar) {
|
|
583
|
+
return null
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Extract map function
|
|
587
|
+
const mapFn = mapCall.arguments[0]
|
|
588
|
+
if (!ts.isArrowFunction(mapFn)) {
|
|
589
|
+
return null
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Extract item variable
|
|
593
|
+
const itemParam = mapFn.parameters[0]
|
|
594
|
+
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
595
|
+
return null
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const itemVar = itemParam.name.text
|
|
599
|
+
|
|
600
|
+
// Extract workflow.do call from map body
|
|
601
|
+
let doCall: ts.CallExpression | null = null
|
|
602
|
+
|
|
603
|
+
if (ts.isCallExpression(mapFn.body)) {
|
|
604
|
+
doCall = mapFn.body
|
|
605
|
+
} else if (ts.isAwaitExpression(mapFn.body)) {
|
|
606
|
+
// Handle: async (email) => await workflow.do(...)
|
|
607
|
+
if (ts.isCallExpression(mapFn.body.expression)) {
|
|
608
|
+
doCall = mapFn.body.expression
|
|
609
|
+
}
|
|
610
|
+
} else if (ts.isBlock(mapFn.body)) {
|
|
611
|
+
// Look for workflow.do in block
|
|
612
|
+
for (const stmt of mapFn.body.statements) {
|
|
613
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
614
|
+
if (ts.isCallExpression(stmt.expression)) {
|
|
615
|
+
doCall = stmt.expression
|
|
616
|
+
break
|
|
617
|
+
} else if (ts.isAwaitExpression(stmt.expression)) {
|
|
618
|
+
// Handle: return await workflow.do(...)
|
|
619
|
+
if (ts.isCallExpression(stmt.expression.expression)) {
|
|
620
|
+
doCall = stmt.expression.expression
|
|
621
|
+
break
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Create a temporary context for the child step
|
|
633
|
+
const childContext: ExtractionContext = {
|
|
634
|
+
...context,
|
|
635
|
+
outputVars: new Map(context.outputVars),
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const childStep = extractRpcStep(doCall, childContext)
|
|
639
|
+
if (!childStep) {
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
type: 'fanout',
|
|
645
|
+
stepName: childStep.stepName,
|
|
646
|
+
sourceVar,
|
|
647
|
+
itemVar,
|
|
648
|
+
mode: 'parallel',
|
|
649
|
+
child: childStep,
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Extract parallel group from Promise.all([...])
|
|
655
|
+
*/
|
|
656
|
+
function extractParallelGroup(
|
|
657
|
+
call: ts.CallExpression,
|
|
658
|
+
context: ExtractionContext
|
|
659
|
+
): ParallelGroupStepMeta | null {
|
|
660
|
+
const arrayArg = call.arguments[0]
|
|
661
|
+
if (!ts.isArrayLiteralExpression(arrayArg)) {
|
|
662
|
+
return null
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const children: RpcStepMeta[] = []
|
|
666
|
+
|
|
667
|
+
for (const elem of arrayArg.elements) {
|
|
668
|
+
if (ts.isCallExpression(elem) && isWorkflowDoCall(elem, context.checker)) {
|
|
669
|
+
const step = extractRpcStep(elem, context)
|
|
670
|
+
if (step) {
|
|
671
|
+
children.push(step)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (children.length === 0) {
|
|
677
|
+
return null
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
type: 'parallel',
|
|
682
|
+
children,
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Extract sequential fanout from for-of loop
|
|
688
|
+
*/
|
|
689
|
+
function extractSequentialFanout(
|
|
690
|
+
statement: ts.ForOfStatement,
|
|
691
|
+
context: ExtractionContext
|
|
692
|
+
): FanoutStepMeta | null {
|
|
693
|
+
if (!isSequentialFanout(statement)) {
|
|
694
|
+
return null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const vars = extractForOfVariable(statement)
|
|
698
|
+
if (!vars) {
|
|
699
|
+
return null
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const { itemVar, sourceVar } = vars
|
|
703
|
+
|
|
704
|
+
// Extract child step and optional sleep from loop body
|
|
705
|
+
if (!ts.isBlock(statement.statement)) {
|
|
706
|
+
return null
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let childStep: RpcStepMeta | null = null
|
|
710
|
+
let timeBetween: string | undefined = undefined
|
|
711
|
+
|
|
712
|
+
for (const stmt of statement.statement.statements) {
|
|
713
|
+
// Look for workflow.do
|
|
714
|
+
if (ts.isExpressionStatement(stmt)) {
|
|
715
|
+
const expr = stmt.expression
|
|
716
|
+
|
|
717
|
+
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
718
|
+
const call = expr.expression
|
|
719
|
+
|
|
720
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
721
|
+
const step = extractRpcStep(call, context)
|
|
722
|
+
if (step) {
|
|
723
|
+
childStep = step
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
728
|
+
// Extract duration for timeBetween
|
|
729
|
+
const args = call.arguments
|
|
730
|
+
if (args.length >= 2) {
|
|
731
|
+
try {
|
|
732
|
+
const numValue = extractNumberLiteral(args[1])
|
|
733
|
+
if (numValue !== null) {
|
|
734
|
+
timeBetween = `${numValue}ms`
|
|
735
|
+
} else {
|
|
736
|
+
timeBetween = extractStringLiteral(args[1], context.checker)
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
// Ignore extraction errors
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Look for if statement with sleep
|
|
747
|
+
if (ts.isIfStatement(stmt)) {
|
|
748
|
+
if (ts.isBlock(stmt.thenStatement)) {
|
|
749
|
+
for (const thenStmt of stmt.thenStatement.statements) {
|
|
750
|
+
if (ts.isExpressionStatement(thenStmt)) {
|
|
751
|
+
const expr = thenStmt.expression
|
|
752
|
+
|
|
753
|
+
if (
|
|
754
|
+
ts.isAwaitExpression(expr) &&
|
|
755
|
+
ts.isCallExpression(expr.expression)
|
|
756
|
+
) {
|
|
757
|
+
const call = expr.expression
|
|
758
|
+
|
|
759
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
760
|
+
const args = call.arguments
|
|
761
|
+
if (args.length >= 2) {
|
|
762
|
+
try {
|
|
763
|
+
const numValue = extractNumberLiteral(args[1])
|
|
764
|
+
if (numValue !== null) {
|
|
765
|
+
timeBetween = `${numValue}ms`
|
|
766
|
+
} else {
|
|
767
|
+
timeBetween = extractStringLiteral(
|
|
768
|
+
args[1],
|
|
769
|
+
context.checker
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
// Ignore extraction errors
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (!childStep) {
|
|
785
|
+
return null
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
type: 'fanout',
|
|
790
|
+
stepName: childStep.stepName,
|
|
791
|
+
sourceVar,
|
|
792
|
+
itemVar,
|
|
793
|
+
mode: 'sequential',
|
|
794
|
+
child: childStep,
|
|
795
|
+
timeBetween,
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Extract return step
|
|
801
|
+
*/
|
|
802
|
+
function extractReturn(
|
|
803
|
+
statement: ts.ReturnStatement,
|
|
804
|
+
context: ExtractionContext
|
|
805
|
+
): ReturnStepMeta | null {
|
|
806
|
+
if (!statement.expression) {
|
|
807
|
+
return null
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
811
|
+
return null
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const outputs: Record<string, OutputBinding> = {}
|
|
815
|
+
|
|
816
|
+
for (const prop of statement.expression.properties) {
|
|
817
|
+
if (
|
|
818
|
+
ts.isPropertyAssignment(prop) ||
|
|
819
|
+
ts.isShorthandPropertyAssignment(prop)
|
|
820
|
+
) {
|
|
821
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null
|
|
822
|
+
if (!propName) {
|
|
823
|
+
continue
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let binding: OutputBinding | null = null
|
|
827
|
+
|
|
828
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
829
|
+
// { orgId } - must be an output variable
|
|
830
|
+
const varName = prop.name.text
|
|
831
|
+
if (context.outputVars.has(varName)) {
|
|
832
|
+
binding = { from: 'outputVar', name: varName }
|
|
833
|
+
}
|
|
834
|
+
} else if (ts.isPropertyAssignment(prop)) {
|
|
835
|
+
const init = prop.initializer
|
|
836
|
+
|
|
837
|
+
// Check for property access (e.g., org.id, owner?.id)
|
|
838
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
839
|
+
const objName = ts.isIdentifier(init.expression)
|
|
840
|
+
? init.expression.text
|
|
841
|
+
: null
|
|
842
|
+
const propPath = init.name.text
|
|
843
|
+
|
|
844
|
+
if (objName && context.outputVars.has(objName)) {
|
|
845
|
+
binding = { from: 'outputVar', name: objName, path: propPath }
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Check for optional chaining (e.g., owner?.id)
|
|
850
|
+
if (
|
|
851
|
+
init.kind === ts.SyntaxKind.PropertyAccessExpression ||
|
|
852
|
+
init.kind === ts.SyntaxKind.NonNullExpression
|
|
853
|
+
) {
|
|
854
|
+
const text = init.getText()
|
|
855
|
+
const match = text.match(/^(\w+)\??\.(\w+)$/)
|
|
856
|
+
if (match) {
|
|
857
|
+
const [, objName, propPath] = match
|
|
858
|
+
if (context.outputVars.has(objName)) {
|
|
859
|
+
binding = { from: 'outputVar', name: objName, path: propPath }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Check for identifier (simple variable reference)
|
|
865
|
+
if (ts.isIdentifier(init)) {
|
|
866
|
+
const varName = init.text
|
|
867
|
+
if (context.outputVars.has(varName)) {
|
|
868
|
+
binding = { from: 'outputVar', name: varName }
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (binding) {
|
|
874
|
+
outputs[propName] = binding
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (Object.keys(outputs).length === 0) {
|
|
880
|
+
return null
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
type: 'return',
|
|
885
|
+
outputs,
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Extract input sources from an argument node
|
|
891
|
+
*/
|
|
892
|
+
function extractInputSources(
|
|
893
|
+
node: ts.Node,
|
|
894
|
+
context: ExtractionContext
|
|
895
|
+
): Record<string, InputSource> | undefined {
|
|
896
|
+
if (!ts.isObjectLiteralExpression(node)) {
|
|
897
|
+
return undefined
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const inputs: Record<string, InputSource> = {}
|
|
901
|
+
|
|
902
|
+
for (const prop of node.properties) {
|
|
903
|
+
if (
|
|
904
|
+
ts.isPropertyAssignment(prop) ||
|
|
905
|
+
ts.isShorthandPropertyAssignment(prop)
|
|
906
|
+
) {
|
|
907
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null
|
|
908
|
+
if (!propName) {
|
|
909
|
+
continue
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let source: InputSource | null = null
|
|
913
|
+
|
|
914
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
915
|
+
// { email } - could be from input or output var
|
|
916
|
+
const varName = prop.name.text
|
|
917
|
+
if (context.outputVars.has(varName)) {
|
|
918
|
+
source = { from: 'outputVar', name: varName }
|
|
919
|
+
} else {
|
|
920
|
+
source = { from: 'input', path: varName }
|
|
921
|
+
}
|
|
922
|
+
} else if (ts.isPropertyAssignment(prop)) {
|
|
923
|
+
source = extractInputSource(prop.initializer, context)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (source) {
|
|
927
|
+
inputs[propName] = source
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
932
|
+
// Handle spread: { ...data }
|
|
933
|
+
if (ts.isIdentifier(prop.expression)) {
|
|
934
|
+
const varName = prop.expression.text
|
|
935
|
+
if (varName === context.inputParamName) {
|
|
936
|
+
// This is spreading the input data
|
|
937
|
+
// We can't fully model this in v1, so we'll skip it
|
|
938
|
+
continue
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return Object.keys(inputs).length > 0 ? inputs : undefined
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Extract a single input source
|
|
949
|
+
*/
|
|
950
|
+
function extractInputSource(
|
|
951
|
+
node: ts.Node,
|
|
952
|
+
context: ExtractionContext
|
|
953
|
+
): InputSource | null {
|
|
954
|
+
// Property access: data.email, org.id
|
|
955
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
956
|
+
const objExpr = node.expression
|
|
957
|
+
const propName = node.name.text
|
|
958
|
+
|
|
959
|
+
if (ts.isIdentifier(objExpr)) {
|
|
960
|
+
const objName = objExpr.text
|
|
961
|
+
|
|
962
|
+
if (objName === context.inputParamName) {
|
|
963
|
+
return { from: 'input', path: propName }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (context.outputVars.has(objName)) {
|
|
967
|
+
return { from: 'outputVar', name: objName, path: propName }
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Identifier: email, orgId
|
|
973
|
+
if (ts.isIdentifier(node)) {
|
|
974
|
+
const varName = node.text
|
|
975
|
+
|
|
976
|
+
if (context.outputVars.has(varName)) {
|
|
977
|
+
return { from: 'outputVar', name: varName }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Assume it's from input
|
|
981
|
+
return { from: 'input', path: varName }
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Literal: "string", 123, true, false, null
|
|
985
|
+
if (
|
|
986
|
+
ts.isStringLiteral(node) ||
|
|
987
|
+
ts.isNumericLiteral(node) ||
|
|
988
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
989
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
990
|
+
node.kind === ts.SyntaxKind.NullKeyword
|
|
991
|
+
) {
|
|
992
|
+
let value: unknown
|
|
993
|
+
if (ts.isStringLiteral(node)) {
|
|
994
|
+
value = node.text
|
|
995
|
+
} else if (ts.isNumericLiteral(node)) {
|
|
996
|
+
value = Number(node.text)
|
|
997
|
+
} else if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
998
|
+
value = true
|
|
999
|
+
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1000
|
+
value = false
|
|
1001
|
+
} else if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
1002
|
+
value = null
|
|
1003
|
+
}
|
|
1004
|
+
return { from: 'literal', value }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Object literal
|
|
1008
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1009
|
+
const obj: Record<string, unknown> = {}
|
|
1010
|
+
for (const prop of node.properties) {
|
|
1011
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
1012
|
+
const propName = prop.name.text
|
|
1013
|
+
const propSource = extractInputSource(prop.initializer, context)
|
|
1014
|
+
if (propSource && propSource.from === 'literal') {
|
|
1015
|
+
obj[propName] = propSource.value
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return { from: 'literal', value: obj }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Array literal
|
|
1023
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1024
|
+
const arr: unknown[] = []
|
|
1025
|
+
for (const elem of node.elements) {
|
|
1026
|
+
const elemSource = extractInputSource(elem, context)
|
|
1027
|
+
if (elemSource && elemSource.from === 'literal') {
|
|
1028
|
+
arr.push(elemSource.value)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return { from: 'literal', value: arr }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return null
|
|
1035
|
+
}
|