@pikku/inspector 0.12.74 → 0.12.81
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 +347 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +23 -13
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2207
- package/src/utils/filter-inspector-state.ts +0 -1302
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,2254 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import type {
|
|
3
|
-
WorkflowStepMeta,
|
|
4
|
-
RpcStepMeta,
|
|
5
|
-
InlineStepMeta,
|
|
6
|
-
BranchStepMeta,
|
|
7
|
-
ParallelGroupStepMeta,
|
|
8
|
-
FanoutStepMeta,
|
|
9
|
-
CancelStepMeta,
|
|
10
|
-
SuspendStepMeta,
|
|
11
|
-
SleepStepMeta,
|
|
12
|
-
ApprovalStepMeta,
|
|
13
|
-
SetStepMeta,
|
|
14
|
-
SwitchStepMeta,
|
|
15
|
-
SwitchCaseMeta,
|
|
16
|
-
FilterStepMeta,
|
|
17
|
-
ArrayPredicateStepMeta,
|
|
18
|
-
InputSource,
|
|
19
|
-
OutputBinding,
|
|
20
|
-
Condition,
|
|
21
|
-
WorkflowContext,
|
|
22
|
-
ContextVariable,
|
|
23
|
-
} from '@pikku/core/workflow'
|
|
24
|
-
import type { ScenarioStepMeta } from '@pikku/core/scenario'
|
|
25
|
-
import {
|
|
26
|
-
isWorkflowDoCall,
|
|
27
|
-
isWorkflowExpectEventuallyCall,
|
|
28
|
-
isScenarioStepCall,
|
|
29
|
-
isScenarioExpectationCall,
|
|
30
|
-
getScenarioStepPhase,
|
|
31
|
-
DYNAMIC_SCENARIO_STEP_TARGET,
|
|
32
|
-
extractActorFromOptions,
|
|
33
|
-
isWorkflowSleepCall,
|
|
34
|
-
isWorkflowSuspendCall,
|
|
35
|
-
isWorkflowApprovalCall,
|
|
36
|
-
isThrowCancelException,
|
|
37
|
-
extractCancelReason,
|
|
38
|
-
isParallelFanout,
|
|
39
|
-
isParallelGroup,
|
|
40
|
-
isSequentialFanout,
|
|
41
|
-
isArrayFilter,
|
|
42
|
-
isArraySome,
|
|
43
|
-
isArrayEvery,
|
|
44
|
-
extractForOfVariable,
|
|
45
|
-
isArrayType,
|
|
46
|
-
getSourceText,
|
|
47
|
-
extractSourcePath,
|
|
48
|
-
} from './patterns.js'
|
|
49
|
-
import type { ValidationError } from './validation.js'
|
|
50
|
-
import {
|
|
51
|
-
validateNoDisallowedPatterns,
|
|
52
|
-
validateAwaitedCalls,
|
|
53
|
-
formatValidationErrors,
|
|
54
|
-
} from './validation.js'
|
|
55
|
-
import {
|
|
56
|
-
extractStringLiteral,
|
|
57
|
-
extractNumberLiteral,
|
|
58
|
-
} from '../../extract-node-value.js'
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Extraction context to track state during AST traversal
|
|
62
|
-
*/
|
|
63
|
-
interface ExtractionContext {
|
|
64
|
-
checker: ts.TypeChecker
|
|
65
|
-
outputVars: Map<string, { type: ts.Type; node: ts.Node }>
|
|
66
|
-
arrayVars: Set<string>
|
|
67
|
-
conditionalVars: Set<string>
|
|
68
|
-
inputParamName: string | null
|
|
69
|
-
errors: ValidationError[]
|
|
70
|
-
/** Loop variables in scope (for fanout item variables) */
|
|
71
|
-
loopVars: Set<string>
|
|
72
|
-
/** Context variables (top-level let/const with simple values) */
|
|
73
|
-
contextVars: Map<string, { type: string; default: unknown }>
|
|
74
|
-
/** Track nesting depth to detect block-scoped vars */
|
|
75
|
-
depth: number
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Result of DSL workflow extraction
|
|
80
|
-
*/
|
|
81
|
-
export interface ExtractionResult {
|
|
82
|
-
status: 'ok' | 'error'
|
|
83
|
-
steps?: WorkflowStepMeta[]
|
|
84
|
-
/** Workflow context (top-level variables) */
|
|
85
|
-
context?: WorkflowContext
|
|
86
|
-
/**
|
|
87
|
-
* The flow asserts something through an expectation helper rather than a
|
|
88
|
-
* `then` step. Recorded separately because those helpers are inline steps and
|
|
89
|
-
* leave no phase behind, so PKU680 has no other way to see them.
|
|
90
|
-
*/
|
|
91
|
-
asserts?: boolean
|
|
92
|
-
reason?: string
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* A function the walk must not step into, because nothing here says it runs.
|
|
97
|
-
*
|
|
98
|
-
* A callback handed straight to a call — a fanout's `map`, an inline
|
|
99
|
-
* `workflow.do` step — executes as part of the body and counts. A function that
|
|
100
|
-
* is merely *declared* or assigned to a name does not: an assertion parked in an
|
|
101
|
-
* unused helper asserts nothing, and counting it would let a scenario silence
|
|
102
|
-
* PKU680 with dead code.
|
|
103
|
-
*/
|
|
104
|
-
function isUncalledFunction(node: ts.Node): boolean {
|
|
105
|
-
if (
|
|
106
|
-
!ts.isFunctionDeclaration(node) &&
|
|
107
|
-
!ts.isFunctionExpression(node) &&
|
|
108
|
-
!ts.isArrowFunction(node)
|
|
109
|
-
) {
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
if (ts.isFunctionDeclaration(node)) return true
|
|
113
|
-
const parent = node.parent
|
|
114
|
-
return !(
|
|
115
|
-
parent &&
|
|
116
|
-
ts.isCallExpression(parent) &&
|
|
117
|
-
parent.arguments.some((argument) => argument === node)
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Whether the body calls one of the expectation helpers anywhere it runs.
|
|
123
|
-
*
|
|
124
|
-
* A whole-body walk rather than a check inside step extraction: the helpers
|
|
125
|
-
* produce no step of their own, so there is no extraction result to hang the
|
|
126
|
-
* answer off, and one nested inside a branch asserts just as much as one at the
|
|
127
|
-
* top level.
|
|
128
|
-
*/
|
|
129
|
-
function hasScenarioExpectation(body: ts.Node): boolean {
|
|
130
|
-
let found = false
|
|
131
|
-
const visit = (node: ts.Node): void => {
|
|
132
|
-
if (found) return
|
|
133
|
-
if (ts.isCallExpression(node) && isScenarioExpectationCall(node)) {
|
|
134
|
-
found = true
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
if (node !== body && isUncalledFunction(node)) return
|
|
138
|
-
ts.forEachChild(node, visit)
|
|
139
|
-
}
|
|
140
|
-
visit(body)
|
|
141
|
-
return found
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Extract DSL workflow metadata from a function declaration
|
|
146
|
-
*/
|
|
147
|
-
export function extractDSLWorkflow(
|
|
148
|
-
funcNode: ts.Node,
|
|
149
|
-
checker: ts.TypeChecker,
|
|
150
|
-
options?: { allowInline?: boolean }
|
|
151
|
-
): ExtractionResult {
|
|
152
|
-
try {
|
|
153
|
-
// Find the async arrow function
|
|
154
|
-
const arrowFunc = findWorkflowFunction(funcNode)
|
|
155
|
-
if (!arrowFunc) {
|
|
156
|
-
return {
|
|
157
|
-
status: 'error',
|
|
158
|
-
reason: 'Could not find async arrow function in workflow definition',
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Extract input parameter name (second parameter)
|
|
163
|
-
const inputParamName = extractInputParamName(arrowFunc)
|
|
164
|
-
if (inputParamName === undefined) {
|
|
165
|
-
return {
|
|
166
|
-
status: 'error',
|
|
167
|
-
reason: 'Could not determine input parameter name',
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Initialize extraction context
|
|
172
|
-
const context: ExtractionContext = {
|
|
173
|
-
checker,
|
|
174
|
-
outputVars: new Map(),
|
|
175
|
-
arrayVars: new Set(),
|
|
176
|
-
conditionalVars: new Set(),
|
|
177
|
-
inputParamName,
|
|
178
|
-
errors: [],
|
|
179
|
-
loopVars: new Set(),
|
|
180
|
-
contextVars: new Map(),
|
|
181
|
-
depth: 0,
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Validate no disallowed patterns
|
|
185
|
-
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body, {
|
|
186
|
-
allowInline: options?.allowInline,
|
|
187
|
-
})
|
|
188
|
-
if (patternErrors.length > 0) {
|
|
189
|
-
return {
|
|
190
|
-
status: 'error',
|
|
191
|
-
reason: formatValidationErrors(patternErrors),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Validate all workflow calls are awaited
|
|
196
|
-
const awaitErrors = validateAwaitedCalls(arrowFunc.body)
|
|
197
|
-
if (awaitErrors.length > 0) {
|
|
198
|
-
return {
|
|
199
|
-
status: 'error',
|
|
200
|
-
reason: formatValidationErrors(awaitErrors),
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Extract steps from function body
|
|
205
|
-
const steps = extractSteps(arrowFunc.body, context)
|
|
206
|
-
|
|
207
|
-
// Check for any accumulated errors
|
|
208
|
-
if (context.errors.length > 0) {
|
|
209
|
-
return {
|
|
210
|
-
status: 'error',
|
|
211
|
-
reason: formatValidationErrors(context.errors),
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Build workflow context from extracted context variables
|
|
216
|
-
const workflowContext: WorkflowContext = {}
|
|
217
|
-
for (const [name, info] of context.contextVars) {
|
|
218
|
-
workflowContext[name] = {
|
|
219
|
-
type: info.type as ContextVariable['type'],
|
|
220
|
-
default: info.default,
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
status: 'ok',
|
|
226
|
-
steps,
|
|
227
|
-
context:
|
|
228
|
-
Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
|
|
229
|
-
asserts: hasScenarioExpectation(arrowFunc.body) || undefined,
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
return {
|
|
233
|
-
status: 'error',
|
|
234
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Find the workflow function (async arrow function)
|
|
241
|
-
*/
|
|
242
|
-
function findWorkflowFunction(node: ts.Node): ts.ArrowFunction | null {
|
|
243
|
-
// Handle pikkuWorkflowFunc(async () => {}) or pikkuWorkflowComplexFunc(async () => {})
|
|
244
|
-
if (ts.isCallExpression(node)) {
|
|
245
|
-
const arg = node.arguments[0]
|
|
246
|
-
if (arg && ts.isArrowFunction(arg)) {
|
|
247
|
-
return arg
|
|
248
|
-
}
|
|
249
|
-
// Also check if first argument is an object with func property
|
|
250
|
-
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
251
|
-
for (const prop of arg.properties) {
|
|
252
|
-
if (
|
|
253
|
-
ts.isPropertyAssignment(prop) &&
|
|
254
|
-
ts.isIdentifier(prop.name) &&
|
|
255
|
-
prop.name.text === 'func'
|
|
256
|
-
) {
|
|
257
|
-
if (ts.isArrowFunction(prop.initializer)) {
|
|
258
|
-
return prop.initializer
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Handle pikkuWorkflowFunc({ func: async () => {} })
|
|
266
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
267
|
-
for (const prop of node.properties) {
|
|
268
|
-
if (
|
|
269
|
-
ts.isPropertyAssignment(prop) &&
|
|
270
|
-
ts.isIdentifier(prop.name) &&
|
|
271
|
-
prop.name.text === 'func'
|
|
272
|
-
) {
|
|
273
|
-
if (ts.isArrowFunction(prop.initializer)) {
|
|
274
|
-
return prop.initializer
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return null
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Extract the input parameter name from the arrow function.
|
|
285
|
-
*
|
|
286
|
-
* Returns `null` when no input parameter is declared at all. That is
|
|
287
|
-
* legitimate — a workflow may ignore its input — and nothing in the body can
|
|
288
|
-
* reference it, so every name comparison against `null` correctly fails.
|
|
289
|
-
* Returns `undefined` when an input parameter is declared but is not a plain
|
|
290
|
-
* identifier (destructured), which the extractor cannot track by name.
|
|
291
|
-
*/
|
|
292
|
-
function extractInputParamName(
|
|
293
|
-
arrowFunc: ts.ArrowFunction
|
|
294
|
-
): string | null | undefined {
|
|
295
|
-
if (arrowFunc.parameters.length < 2) {
|
|
296
|
-
return null
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const secondParam = arrowFunc.parameters[1]
|
|
300
|
-
if (ts.isIdentifier(secondParam.name)) {
|
|
301
|
-
return secondParam.name.text
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return undefined
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Extract steps from the function body
|
|
309
|
-
*/
|
|
310
|
-
function extractSteps(
|
|
311
|
-
body: ts.Node,
|
|
312
|
-
context: ExtractionContext,
|
|
313
|
-
incrementDepth = false
|
|
314
|
-
): WorkflowStepMeta[] {
|
|
315
|
-
const steps: WorkflowStepMeta[] = []
|
|
316
|
-
|
|
317
|
-
if (!ts.isBlock(body)) {
|
|
318
|
-
return steps
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Increment depth when entering a nested block
|
|
322
|
-
if (incrementDepth) {
|
|
323
|
-
context.depth++
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
for (const statement of body.statements) {
|
|
327
|
-
const extracted = extractStep(statement, context)
|
|
328
|
-
if (extracted) {
|
|
329
|
-
steps.push(extracted)
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Restore depth
|
|
334
|
-
if (incrementDepth) {
|
|
335
|
-
context.depth--
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return steps
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Extract a single step from a statement
|
|
343
|
-
*/
|
|
344
|
-
function extractStep(
|
|
345
|
-
statement: ts.Statement,
|
|
346
|
-
context: ExtractionContext
|
|
347
|
-
): WorkflowStepMeta | null {
|
|
348
|
-
// Variable declaration with workflow.do assignment
|
|
349
|
-
if (ts.isVariableStatement(statement)) {
|
|
350
|
-
return extractVariableDeclaration(statement, context)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Expression statement (await workflow.do without assignment)
|
|
354
|
-
if (ts.isExpressionStatement(statement)) {
|
|
355
|
-
return extractExpressionStatement(statement, context)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// If statement (branch)
|
|
359
|
-
if (ts.isIfStatement(statement)) {
|
|
360
|
-
return extractBranch(statement, context)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Switch statement
|
|
364
|
-
if (ts.isSwitchStatement(statement)) {
|
|
365
|
-
return extractSwitch(statement, context)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// For-of statement (sequential fanout)
|
|
369
|
-
if (ts.isForOfStatement(statement)) {
|
|
370
|
-
const reason: { message?: string } = {}
|
|
371
|
-
const fanout = extractSequentialFanout(statement, context, reason)
|
|
372
|
-
if (!fanout && containsWorkflowCall(statement.statement, context.checker)) {
|
|
373
|
-
// A for-of the DSL can't model as a sequential fanout (its iterable is not
|
|
374
|
-
// a data-array identifier/field — e.g. a counting loop
|
|
375
|
-
// `for (const i of [...Array(N).keys()])`). Silently returning null here
|
|
376
|
-
// used to drop the loop AND every `workflow.do` inside it, so the workflow
|
|
377
|
-
// serialized with zero steps and its step functions were never registered
|
|
378
|
-
// (→ runtime "Function not found"). Fail loudly instead: pikkuWorkflowFunc
|
|
379
|
-
// reports INVALID_DSL_WORKFLOW; pikkuWorkflowComplexFunc falls back to the
|
|
380
|
-
// basic AST walk (which DOES register loop-invoked functions), so a genuine
|
|
381
|
-
// control-flow loop belongs in a complex workflow.
|
|
382
|
-
//
|
|
383
|
-
// Guarded on the body actually containing a workflow call: a loop that only
|
|
384
|
-
// massages locals (building a lookup, summing a total) has no step to lose,
|
|
385
|
-
// and erroring on it would be a false positive — the whole hazard is a
|
|
386
|
-
// dropped `workflow.do`, not a dropped `for`.
|
|
387
|
-
context.errors.push({
|
|
388
|
-
message:
|
|
389
|
-
`The for-of loop '${statement.getText().slice(0, 60)}' can't be expressed in a DSL workflow — ` +
|
|
390
|
-
(reason.message ??
|
|
391
|
-
`its iterable must be a data array (an identifier or field like 'data.items'), not a computed/inline ` +
|
|
392
|
-
`iterable such as '[...Array(n).keys()]'. Use pikkuWorkflowComplexFunc for a control-flow/counting loop, ` +
|
|
393
|
-
`or iterate over a real input array.`),
|
|
394
|
-
node: statement,
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
return fanout
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Return statement
|
|
401
|
-
if (ts.isReturnStatement(statement)) {
|
|
402
|
-
return extractReturn(statement, context)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Throw statement (for WorkflowCancelledException)
|
|
406
|
-
if (ts.isThrowStatement(statement)) {
|
|
407
|
-
return extractThrowCancel(statement, context)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return null
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Extract variable declaration (const x = await workflow.do(...))
|
|
415
|
-
*/
|
|
416
|
-
function extractVariableDeclaration(
|
|
417
|
-
statement: ts.VariableStatement,
|
|
418
|
-
context: ExtractionContext
|
|
419
|
-
): WorkflowStepMeta | null {
|
|
420
|
-
const declList = statement.declarationList
|
|
421
|
-
if (declList.declarations.length !== 1) {
|
|
422
|
-
context.errors.push({
|
|
423
|
-
message: `A single declaration statement may only declare one variable in DSL workflows. Split '${statement.getText().slice(0, 60)}' into separate statements.`,
|
|
424
|
-
node: statement,
|
|
425
|
-
})
|
|
426
|
-
return null
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const decl = declList.declarations[0]
|
|
430
|
-
if (!ts.isIdentifier(decl.name)) {
|
|
431
|
-
return extractDestructuredDeclaration(statement, decl, context)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const varName = decl.name.text
|
|
435
|
-
const init = decl.initializer
|
|
436
|
-
|
|
437
|
-
// Check for block-scoped variable declarations (not allowed)
|
|
438
|
-
if (context.depth > 0) {
|
|
439
|
-
context.errors.push({
|
|
440
|
-
message: `Variable declaration '${varName}' inside block is not supported in DSL workflows. Move all let/const declarations to the top level.`,
|
|
441
|
-
node: statement,
|
|
442
|
-
})
|
|
443
|
-
return null
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (!init) {
|
|
447
|
-
return null
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Check for simple literal/expression context variable (let x = 'value')
|
|
451
|
-
const literalValue = extractLiteralValue(init)
|
|
452
|
-
if (literalValue !== undefined) {
|
|
453
|
-
const tsType = context.checker.getTypeAtLocation(decl)
|
|
454
|
-
const typeStr = inferSimpleType(tsType, context.checker)
|
|
455
|
-
context.contextVars.set(varName, { type: typeStr, default: literalValue })
|
|
456
|
-
return null // No step emitted, just register the context var
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Check for await workflow.do(...)
|
|
460
|
-
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
461
|
-
const call = init.expression
|
|
462
|
-
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
463
|
-
const step = isScenarioStepCall(call)
|
|
464
|
-
? extractScenarioStep(call, context, varName)
|
|
465
|
-
: isInlineDoCall(call)
|
|
466
|
-
? extractInlineStep(call, context)
|
|
467
|
-
: extractRpcStep(call, context, varName)
|
|
468
|
-
if (step) {
|
|
469
|
-
// Track output variable
|
|
470
|
-
const type = context.checker.getTypeAtLocation(decl)
|
|
471
|
-
context.outputVars.set(varName, { type, node: decl })
|
|
472
|
-
|
|
473
|
-
// Check if it's an array type
|
|
474
|
-
if (isArrayType(type, context.checker)) {
|
|
475
|
-
context.arrayVars.add(varName)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Check if it's a conditional variable (let x: T | undefined)
|
|
479
|
-
if (declList.flags & ts.NodeFlags.Let) {
|
|
480
|
-
const typeNode = decl.type
|
|
481
|
-
if (typeNode && ts.isUnionTypeNode(typeNode)) {
|
|
482
|
-
// Check if union includes undefined
|
|
483
|
-
const hasUndefined = typeNode.types.some(
|
|
484
|
-
(t) =>
|
|
485
|
-
(ts.isLiteralTypeNode(t) &&
|
|
486
|
-
t.literal.kind === ts.SyntaxKind.UndefinedKeyword) ||
|
|
487
|
-
t.kind === ts.SyntaxKind.UndefinedKeyword
|
|
488
|
-
)
|
|
489
|
-
if (hasUndefined) {
|
|
490
|
-
context.conditionalVars.add(varName)
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return step
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Promise.all fanout/group captured into a variable
|
|
500
|
-
// (const results = await Promise.all(array.map(...)))
|
|
501
|
-
if (isParallelFanout(call) || isParallelGroup(call)) {
|
|
502
|
-
const step = isParallelFanout(call)
|
|
503
|
-
? extractParallelFanout(call, context)
|
|
504
|
-
: extractParallelGroup(call, context)
|
|
505
|
-
if (step) {
|
|
506
|
-
const type = context.checker.getTypeAtLocation(decl)
|
|
507
|
-
context.outputVars.set(varName, { type, node: decl })
|
|
508
|
-
if (isArrayType(type, context.checker)) {
|
|
509
|
-
context.arrayVars.add(varName)
|
|
510
|
-
}
|
|
511
|
-
return step
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Check for array.filter(...)
|
|
517
|
-
if (ts.isCallExpression(init)) {
|
|
518
|
-
if (isArrayFilter(init)) {
|
|
519
|
-
const filterStep = extractArrayFilter(init, context, varName)
|
|
520
|
-
if (filterStep) {
|
|
521
|
-
const type = context.checker.getTypeAtLocation(decl)
|
|
522
|
-
context.outputVars.set(varName, { type, node: decl })
|
|
523
|
-
if (isArrayType(type, context.checker)) {
|
|
524
|
-
context.arrayVars.add(varName)
|
|
525
|
-
}
|
|
526
|
-
return filterStep
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (isArraySome(init) || isArrayEvery(init)) {
|
|
531
|
-
const predicateStep = extractArrayPredicate(init, context, varName)
|
|
532
|
-
if (predicateStep) {
|
|
533
|
-
const type = context.checker.getTypeAtLocation(decl)
|
|
534
|
-
context.outputVars.set(varName, { type, node: decl })
|
|
535
|
-
return predicateStep
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return null
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Extract a declaration whose binding is a destructuring pattern.
|
|
545
|
-
*
|
|
546
|
-
* `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
|
|
547
|
-
* in parallel and keep both results, so each element of the pattern is bound to
|
|
548
|
-
* the matching child step's output. Every other destructuring OF A STEP reports a
|
|
549
|
-
* diagnostic rather than silently dropping the step.
|
|
550
|
-
*
|
|
551
|
-
* A destructure whose initializer is NOT a step is just an ordinary local binding
|
|
552
|
-
* — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
|
|
553
|
-
* with step results. Those pass through as non-steps, exactly as the identifier
|
|
554
|
-
* path already does for `const x = someLocal`. Reporting them was rejecting the
|
|
555
|
-
* single most idiomatic line in a DSL workflow (destructuring the workflow's own
|
|
556
|
-
* input), under a message about step results that named nothing the author wrote.
|
|
557
|
-
*/
|
|
558
|
-
function extractDestructuredDeclaration(
|
|
559
|
-
statement: ts.VariableStatement,
|
|
560
|
-
decl: ts.VariableDeclaration,
|
|
561
|
-
context: ExtractionContext
|
|
562
|
-
): WorkflowStepMeta | null {
|
|
563
|
-
const init = decl.initializer
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
init &&
|
|
567
|
-
ts.isAwaitExpression(init) &&
|
|
568
|
-
ts.isCallExpression(init.expression) &&
|
|
569
|
-
isParallelGroup(init.expression) &&
|
|
570
|
-
ts.isArrayBindingPattern(decl.name)
|
|
571
|
-
) {
|
|
572
|
-
const elements = decl.name.elements
|
|
573
|
-
const allSimple = elements.every(
|
|
574
|
-
(el) =>
|
|
575
|
-
ts.isBindingElement(el) &&
|
|
576
|
-
ts.isIdentifier(el.name) &&
|
|
577
|
-
!el.dotDotDotToken
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
if (allSimple) {
|
|
581
|
-
const step = extractParallelGroup(init.expression, context)
|
|
582
|
-
if (step) {
|
|
583
|
-
if (elements.length !== step.children.length) {
|
|
584
|
-
context.errors.push({
|
|
585
|
-
message: `Destructuring binds ${elements.length} name(s) but Promise.all has ${step.children.length} step(s). They must match so each result can be bound to its step.`,
|
|
586
|
-
node: statement,
|
|
587
|
-
})
|
|
588
|
-
return null
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
elements.forEach((el, i) => {
|
|
592
|
-
const name = (el as ts.BindingElement).name as ts.Identifier
|
|
593
|
-
const varName = name.text
|
|
594
|
-
step.children[i].outputVar = varName
|
|
595
|
-
const type = context.checker.getTypeAtLocation(name)
|
|
596
|
-
context.outputVars.set(varName, { type, node: name })
|
|
597
|
-
if (isArrayType(type, context.checker)) {
|
|
598
|
-
context.arrayVars.add(varName)
|
|
599
|
-
}
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
return step
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Only a destructure OF A STEP is an error. Anything else is a plain local
|
|
608
|
-
// binding the DSL simply doesn't model as a step.
|
|
609
|
-
const stepCall =
|
|
610
|
-
init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
|
|
611
|
-
? init.expression
|
|
612
|
-
: null
|
|
613
|
-
const destructuresAStep =
|
|
614
|
-
stepCall !== null &&
|
|
615
|
-
(isWorkflowDoCall(stepCall, context.checker) ||
|
|
616
|
-
isScenarioStepCall(stepCall) ||
|
|
617
|
-
isParallelGroup(stepCall) ||
|
|
618
|
-
isParallelFanout(stepCall))
|
|
619
|
-
if (!destructuresAStep) return null
|
|
620
|
-
|
|
621
|
-
context.errors.push({
|
|
622
|
-
message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
|
|
623
|
-
node: statement,
|
|
624
|
-
})
|
|
625
|
-
return null
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Extract expression statement (await workflow.do(...) without assignment)
|
|
630
|
-
*/
|
|
631
|
-
function extractExpressionStatement(
|
|
632
|
-
statement: ts.ExpressionStatement,
|
|
633
|
-
context: ExtractionContext
|
|
634
|
-
): WorkflowStepMeta | null {
|
|
635
|
-
let expr = statement.expression
|
|
636
|
-
|
|
637
|
-
// Handle assignment: x = value or x = await workflow.do(...)
|
|
638
|
-
let outputVar: string | undefined
|
|
639
|
-
if (
|
|
640
|
-
ts.isBinaryExpression(expr) &&
|
|
641
|
-
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
642
|
-
) {
|
|
643
|
-
// Extract variable name from left side
|
|
644
|
-
if (ts.isIdentifier(expr.left)) {
|
|
645
|
-
outputVar = expr.left.text
|
|
646
|
-
|
|
647
|
-
// Check if this is an assignment to a context variable (set step)
|
|
648
|
-
// But if the RHS is a workflow.do() call, fall through to RPC extraction —
|
|
649
|
-
// reassigning a pre-declared variable with a workflow step is valid and common.
|
|
650
|
-
if (context.contextVars.has(outputVar)) {
|
|
651
|
-
const rhs = expr.right
|
|
652
|
-
const rhsCall =
|
|
653
|
-
ts.isAwaitExpression(rhs) && ts.isCallExpression(rhs.expression)
|
|
654
|
-
? rhs.expression
|
|
655
|
-
: null
|
|
656
|
-
const isWorkflowCall = rhsCall
|
|
657
|
-
? isWorkflowDoCall(rhsCall, context.checker)
|
|
658
|
-
: false
|
|
659
|
-
|
|
660
|
-
if (!isWorkflowCall) {
|
|
661
|
-
const literalValue = extractLiteralValue(expr.right)
|
|
662
|
-
if (literalValue !== undefined) {
|
|
663
|
-
return {
|
|
664
|
-
type: 'set',
|
|
665
|
-
variable: outputVar,
|
|
666
|
-
value: literalValue,
|
|
667
|
-
} as SetStepMeta
|
|
668
|
-
}
|
|
669
|
-
// Non-literal assignment to context var - keep the source expression
|
|
670
|
-
return {
|
|
671
|
-
type: 'set',
|
|
672
|
-
variable: outputVar,
|
|
673
|
-
expression: getSourceText(expr.right),
|
|
674
|
-
} as SetStepMeta
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
// Use right side as the expression to extract from
|
|
679
|
-
expr = expr.right
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// await workflow.do(...)
|
|
683
|
-
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
684
|
-
const call = expr.expression
|
|
685
|
-
|
|
686
|
-
if (isWorkflowDoCall(call, context.checker) || isScenarioStepCall(call)) {
|
|
687
|
-
const step = isScenarioStepCall(call)
|
|
688
|
-
? extractScenarioStep(call, context, outputVar)
|
|
689
|
-
: isInlineDoCall(call)
|
|
690
|
-
? extractInlineStep(call, context)
|
|
691
|
-
: extractRpcStep(call, context, outputVar)
|
|
692
|
-
|
|
693
|
-
// Track output variable if this is an assignment
|
|
694
|
-
if (outputVar && step) {
|
|
695
|
-
const type = context.checker.getTypeAtLocation(expr)
|
|
696
|
-
context.outputVars.set(outputVar, { type, node: expr })
|
|
697
|
-
|
|
698
|
-
// Check if it's an array type
|
|
699
|
-
if (isArrayType(type, context.checker)) {
|
|
700
|
-
context.arrayVars.add(outputVar)
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
return step
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (isWorkflowSleepCall(call, context.checker)) {
|
|
708
|
-
return extractSleepStep(call, context)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (isWorkflowSuspendCall(call, context.checker)) {
|
|
712
|
-
return extractSuspendStep(call, context)
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
if (isWorkflowApprovalCall(call, context.checker)) {
|
|
716
|
-
const step = extractApprovalStep(call, context, outputVar)
|
|
717
|
-
|
|
718
|
-
// Unlike suspend, an approval yields a value, so a downstream step can
|
|
719
|
-
// reference it — track the binding the same way workflow.do() does.
|
|
720
|
-
if (outputVar && step) {
|
|
721
|
-
const type = context.checker.getTypeAtLocation(expr)
|
|
722
|
-
context.outputVars.set(outputVar, { type, node: expr })
|
|
723
|
-
if (isArrayType(type, context.checker)) {
|
|
724
|
-
context.arrayVars.add(outputVar)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return step
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Check for parallel group or fanout
|
|
732
|
-
if (isParallelFanout(call)) {
|
|
733
|
-
return extractParallelFanout(call, context)
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (isParallelGroup(call)) {
|
|
737
|
-
return extractParallelGroup(call, context)
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return null
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Extract RPC step from workflow.do() call
|
|
746
|
-
*/
|
|
747
|
-
function extractRpcStep(
|
|
748
|
-
call: ts.CallExpression,
|
|
749
|
-
context: ExtractionContext,
|
|
750
|
-
outputVar?: string
|
|
751
|
-
): RpcStepMeta | null {
|
|
752
|
-
const args = call.arguments
|
|
753
|
-
|
|
754
|
-
if (args.length < 2) {
|
|
755
|
-
return null
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
const stepName = extractStringLiteral(args[0], context.checker)
|
|
760
|
-
const rpcName = extractStringLiteral(args[1], context.checker)
|
|
761
|
-
|
|
762
|
-
// Extract inputs from third argument
|
|
763
|
-
const inputs =
|
|
764
|
-
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
765
|
-
|
|
766
|
-
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
767
|
-
const expectEventually = isWorkflowExpectEventuallyCall(call)
|
|
768
|
-
const optionsIndex = expectEventually ? 4 : 3
|
|
769
|
-
const optionsArg =
|
|
770
|
-
args.length > optionsIndex ? args[optionsIndex] : undefined
|
|
771
|
-
|
|
772
|
-
const options =
|
|
773
|
-
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
774
|
-
? extractStepOptions(optionsArg, context)
|
|
775
|
-
: undefined
|
|
776
|
-
|
|
777
|
-
return {
|
|
778
|
-
type: 'rpc',
|
|
779
|
-
stepName,
|
|
780
|
-
rpcName,
|
|
781
|
-
outputVar,
|
|
782
|
-
inputs,
|
|
783
|
-
options,
|
|
784
|
-
actor: extractActorFromOptions(optionsArg),
|
|
785
|
-
expectEventually: expectEventually || undefined,
|
|
786
|
-
}
|
|
787
|
-
} catch (error) {
|
|
788
|
-
context.errors.push({
|
|
789
|
-
message: `Failed to extract RPC step: ${error instanceof Error ? error.message : String(error)}`,
|
|
790
|
-
node: call,
|
|
791
|
-
})
|
|
792
|
-
return null
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Extract a scenario step from scenario.step/given/when/then(stepName,
|
|
798
|
-
* stepFunc, data?, options?).
|
|
799
|
-
*
|
|
800
|
-
* The step target must be a static string literal so it can be bundled, typed
|
|
801
|
-
* and drawn. When it isn't, the step is still recorded — with a `<dynamic>`
|
|
802
|
-
* target — so post-processing can raise PKU678 rather than the call silently
|
|
803
|
-
* vanishing from the graph.
|
|
804
|
-
*/
|
|
805
|
-
function extractScenarioStep(
|
|
806
|
-
call: ts.CallExpression,
|
|
807
|
-
context: ExtractionContext,
|
|
808
|
-
outputVar?: string
|
|
809
|
-
): ScenarioStepMeta | null {
|
|
810
|
-
const phase = getScenarioStepPhase(call)
|
|
811
|
-
if (!phase) {
|
|
812
|
-
return null
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const args = call.arguments
|
|
816
|
-
if (args.length < 2) {
|
|
817
|
-
return null
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
try {
|
|
821
|
-
const stepName = extractStringLiteral(args[0], context.checker)
|
|
822
|
-
|
|
823
|
-
let stepFunc = DYNAMIC_SCENARIO_STEP_TARGET
|
|
824
|
-
try {
|
|
825
|
-
stepFunc = extractStringLiteral(args[1], context.checker)
|
|
826
|
-
} catch {
|
|
827
|
-
// Left as '<dynamic>' — validated in post-processing (PKU678).
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const inputs =
|
|
831
|
-
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
832
|
-
|
|
833
|
-
const optionsArg = args.length >= 4 ? args[3] : undefined
|
|
834
|
-
const options =
|
|
835
|
-
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
836
|
-
? extractStepOptions(optionsArg, context)
|
|
837
|
-
: undefined
|
|
838
|
-
|
|
839
|
-
return {
|
|
840
|
-
type: 'scenarioStep',
|
|
841
|
-
stepName,
|
|
842
|
-
stepFunc,
|
|
843
|
-
phase,
|
|
844
|
-
outputVar,
|
|
845
|
-
inputs,
|
|
846
|
-
options,
|
|
847
|
-
actor: extractActorFromOptions(optionsArg),
|
|
848
|
-
}
|
|
849
|
-
} catch (error) {
|
|
850
|
-
context.errors.push({
|
|
851
|
-
message: `Failed to extract scenario step: ${error instanceof Error ? error.message : String(error)}`,
|
|
852
|
-
node: call,
|
|
853
|
-
})
|
|
854
|
-
return null
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/**
|
|
859
|
-
* Extract inline step from workflow.do() call with a function argument
|
|
860
|
-
*/
|
|
861
|
-
function extractInlineStep(
|
|
862
|
-
call: ts.CallExpression,
|
|
863
|
-
context: ExtractionContext
|
|
864
|
-
): InlineStepMeta | null {
|
|
865
|
-
const args = call.arguments
|
|
866
|
-
if (args.length < 2) return null
|
|
867
|
-
|
|
868
|
-
try {
|
|
869
|
-
const stepName = extractStringLiteral(args[0], context.checker)
|
|
870
|
-
const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined
|
|
871
|
-
const options =
|
|
872
|
-
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
873
|
-
? extractStepOptions(optionsArg, context)
|
|
874
|
-
: undefined
|
|
875
|
-
|
|
876
|
-
return {
|
|
877
|
-
type: 'inline',
|
|
878
|
-
stepName,
|
|
879
|
-
options,
|
|
880
|
-
}
|
|
881
|
-
} catch (error) {
|
|
882
|
-
context.errors.push({
|
|
883
|
-
message: `Failed to extract inline step: ${error instanceof Error ? error.message : String(error)}`,
|
|
884
|
-
node: call,
|
|
885
|
-
})
|
|
886
|
-
return null
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Check if the second argument of a workflow.do() call is a function
|
|
892
|
-
*/
|
|
893
|
-
function isInlineDoCall(call: ts.CallExpression): boolean {
|
|
894
|
-
const secondArg = call.arguments[1]
|
|
895
|
-
return (
|
|
896
|
-
!!secondArg &&
|
|
897
|
-
(ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg))
|
|
898
|
-
)
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Extract step options from options object
|
|
903
|
-
*/
|
|
904
|
-
function extractStepOptions(
|
|
905
|
-
optionsNode: ts.ObjectLiteralExpression,
|
|
906
|
-
context: ExtractionContext
|
|
907
|
-
): RpcStepMeta['options'] {
|
|
908
|
-
const options: RpcStepMeta['options'] = {}
|
|
909
|
-
|
|
910
|
-
for (const prop of optionsNode.properties) {
|
|
911
|
-
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
912
|
-
const propName = prop.name.text
|
|
913
|
-
|
|
914
|
-
if (propName === 'retries') {
|
|
915
|
-
const retries = extractNumberLiteral(prop.initializer)
|
|
916
|
-
if (retries !== null) {
|
|
917
|
-
options.retries = retries
|
|
918
|
-
}
|
|
919
|
-
} else if (propName === 'retryDelay') {
|
|
920
|
-
try {
|
|
921
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
922
|
-
options.retryDelay = prop.initializer.text
|
|
923
|
-
} else {
|
|
924
|
-
const delay = extractNumberLiteral(prop.initializer)
|
|
925
|
-
if (delay !== null) {
|
|
926
|
-
options.retryDelay = delay
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
// Ignore extraction errors for retryDelay
|
|
931
|
-
}
|
|
932
|
-
} else if (propName === 'onError') {
|
|
933
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
934
|
-
options.onError = prop.initializer.text
|
|
935
|
-
} else {
|
|
936
|
-
context.errors.push({
|
|
937
|
-
message:
|
|
938
|
-
'onError must be a literal RPC name so it can be wired and drawn in the graph.',
|
|
939
|
-
node: prop.initializer,
|
|
940
|
-
})
|
|
941
|
-
}
|
|
942
|
-
} else if (propName === 'description') {
|
|
943
|
-
try {
|
|
944
|
-
options.description = extractStringLiteral(
|
|
945
|
-
prop.initializer,
|
|
946
|
-
context.checker
|
|
947
|
-
)
|
|
948
|
-
} catch {
|
|
949
|
-
// Ignore extraction errors for description
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
return Object.keys(options).length > 0 ? options : undefined
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Extract sleep step from workflow.sleep() call
|
|
960
|
-
*/
|
|
961
|
-
function extractSleepStep(
|
|
962
|
-
call: ts.CallExpression,
|
|
963
|
-
context: ExtractionContext
|
|
964
|
-
): WorkflowStepMeta | null {
|
|
965
|
-
const args = call.arguments
|
|
966
|
-
|
|
967
|
-
if (args.length < 2) {
|
|
968
|
-
return null
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
try {
|
|
972
|
-
const stepName = extractStringLiteral(args[0], context.checker)
|
|
973
|
-
let duration: string | number
|
|
974
|
-
|
|
975
|
-
const numValue = extractNumberLiteral(args[1])
|
|
976
|
-
if (numValue !== null) {
|
|
977
|
-
duration = numValue
|
|
978
|
-
} else {
|
|
979
|
-
try {
|
|
980
|
-
duration = extractStringLiteral(args[1], context.checker)
|
|
981
|
-
} catch {
|
|
982
|
-
// A duration computed at runtime (a loop variable, a field off the
|
|
983
|
-
// input) is legal: the closure evaluates it. Record the source text so
|
|
984
|
-
// the graph can show what it waits on, rather than failing the workflow.
|
|
985
|
-
return {
|
|
986
|
-
type: 'sleep',
|
|
987
|
-
stepName,
|
|
988
|
-
duration: '',
|
|
989
|
-
expression: args[1].getText(),
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
return {
|
|
995
|
-
type: 'sleep',
|
|
996
|
-
stepName,
|
|
997
|
-
duration,
|
|
998
|
-
}
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
context.errors.push({
|
|
1001
|
-
message: `Failed to extract sleep step: ${error instanceof Error ? error.message : String(error)}`,
|
|
1002
|
-
node: call,
|
|
1003
|
-
})
|
|
1004
|
-
return null
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Extract suspend step from workflow.suspend() call
|
|
1010
|
-
*/
|
|
1011
|
-
function extractSuspendStep(
|
|
1012
|
-
call: ts.CallExpression,
|
|
1013
|
-
context: ExtractionContext
|
|
1014
|
-
): SuspendStepMeta | null {
|
|
1015
|
-
const args = call.arguments
|
|
1016
|
-
if (args.length < 1) return null
|
|
1017
|
-
|
|
1018
|
-
try {
|
|
1019
|
-
const reason = extractStringLiteral(args[0], context.checker)
|
|
1020
|
-
return {
|
|
1021
|
-
type: 'suspend',
|
|
1022
|
-
reason,
|
|
1023
|
-
}
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
context.errors.push({
|
|
1026
|
-
message: `Failed to extract suspend step: ${error instanceof Error ? error.message : String(error)}`,
|
|
1027
|
-
node: call,
|
|
1028
|
-
})
|
|
1029
|
-
return null
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Extract approval step from workflow.approval() call
|
|
1035
|
-
*/
|
|
1036
|
-
function extractApprovalStep(
|
|
1037
|
-
call: ts.CallExpression,
|
|
1038
|
-
context: ExtractionContext,
|
|
1039
|
-
outputVar?: string
|
|
1040
|
-
): ApprovalStepMeta | null {
|
|
1041
|
-
const args = call.arguments
|
|
1042
|
-
if (args.length < 1) return null
|
|
1043
|
-
|
|
1044
|
-
try {
|
|
1045
|
-
const reason = extractStringLiteral(args[0], context.checker)
|
|
1046
|
-
const step: ApprovalStepMeta = {
|
|
1047
|
-
type: 'approval',
|
|
1048
|
-
reason,
|
|
1049
|
-
}
|
|
1050
|
-
if (outputVar) {
|
|
1051
|
-
step.outputVar = outputVar
|
|
1052
|
-
}
|
|
1053
|
-
// The `schema` option is a runtime value validated inside the workflow body,
|
|
1054
|
-
// so it is deliberately not serialized here — only the literal options the
|
|
1055
|
-
// graph and planned-step ladder need in order to describe the gate.
|
|
1056
|
-
const options = args[1]
|
|
1057
|
-
if (options && ts.isObjectLiteralExpression(options)) {
|
|
1058
|
-
for (const prop of options.properties) {
|
|
1059
|
-
if (!ts.isPropertyAssignment(prop)) continue
|
|
1060
|
-
const name = prop.name.getText()
|
|
1061
|
-
if (
|
|
1062
|
-
name === 'expiry' &&
|
|
1063
|
-
(ts.isStringLiteral(prop.initializer) ||
|
|
1064
|
-
ts.isNumericLiteral(prop.initializer))
|
|
1065
|
-
) {
|
|
1066
|
-
step.expiry = ts.isNumericLiteral(prop.initializer)
|
|
1067
|
-
? Number(prop.initializer.text)
|
|
1068
|
-
: prop.initializer.text
|
|
1069
|
-
}
|
|
1070
|
-
if (name === 'approvers' && ts.isStringLiteral(prop.initializer)) {
|
|
1071
|
-
step.approvers = prop.initializer
|
|
1072
|
-
.text as ApprovalStepMeta['approvers']
|
|
1073
|
-
}
|
|
1074
|
-
if (name === 'approverScope' && ts.isStringLiteral(prop.initializer)) {
|
|
1075
|
-
step.approverScope = prop.initializer.text
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
return step
|
|
1080
|
-
} catch (error) {
|
|
1081
|
-
context.errors.push({
|
|
1082
|
-
message: `Failed to extract approval step: ${error instanceof Error ? error.message : String(error)}`,
|
|
1083
|
-
node: call,
|
|
1084
|
-
})
|
|
1085
|
-
return null
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
* Extract cancel step from throw WorkflowCancelledException statement
|
|
1091
|
-
*/
|
|
1092
|
-
function extractThrowCancel(
|
|
1093
|
-
statement: ts.ThrowStatement,
|
|
1094
|
-
context: ExtractionContext
|
|
1095
|
-
): CancelStepMeta | null {
|
|
1096
|
-
if (!isThrowCancelException(statement)) {
|
|
1097
|
-
return null
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
const reason = extractCancelReason(statement, context.checker)
|
|
1101
|
-
return {
|
|
1102
|
-
type: 'cancel',
|
|
1103
|
-
reason,
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Parse a condition expression into a Condition structure
|
|
1109
|
-
*/
|
|
1110
|
-
function parseCondition(expr: ts.Expression): Condition {
|
|
1111
|
-
// Handle binary expressions (&&, ||)
|
|
1112
|
-
if (ts.isBinaryExpression(expr)) {
|
|
1113
|
-
const operator = expr.operatorToken.kind
|
|
1114
|
-
|
|
1115
|
-
// AND operator (&&)
|
|
1116
|
-
if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1117
|
-
return {
|
|
1118
|
-
type: 'and',
|
|
1119
|
-
conditions: [parseCondition(expr.left), parseCondition(expr.right)],
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// OR operator (||)
|
|
1124
|
-
if (operator === ts.SyntaxKind.BarBarToken) {
|
|
1125
|
-
return {
|
|
1126
|
-
type: 'or',
|
|
1127
|
-
conditions: [parseCondition(expr.left), parseCondition(expr.right)],
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Handle parenthesized expressions - unwrap and parse inner
|
|
1133
|
-
if (ts.isParenthesizedExpression(expr)) {
|
|
1134
|
-
return parseCondition(expr.expression)
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Simple condition (comparison, function call, variable, etc.)
|
|
1138
|
-
return {
|
|
1139
|
-
type: 'simple',
|
|
1140
|
-
expression: getSourceText(expr),
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Extract branch step from if statement (supports if/else-if/else chains)
|
|
1146
|
-
*/
|
|
1147
|
-
function extractBranch(
|
|
1148
|
-
statement: ts.IfStatement,
|
|
1149
|
-
context: ExtractionContext
|
|
1150
|
-
): BranchStepMeta | null {
|
|
1151
|
-
const branches: BranchStepMeta['branches'] = []
|
|
1152
|
-
let elseSteps: BranchStepMeta['elseSteps']
|
|
1153
|
-
|
|
1154
|
-
// Walk the if/else-if chain
|
|
1155
|
-
let current: ts.IfStatement | undefined = statement
|
|
1156
|
-
while (current) {
|
|
1157
|
-
const condition = parseCondition(current.expression)
|
|
1158
|
-
const steps = ts.isBlock(current.thenStatement)
|
|
1159
|
-
? extractSteps(current.thenStatement, context, true)
|
|
1160
|
-
: extractStepsFromStatement(current.thenStatement, context)
|
|
1161
|
-
|
|
1162
|
-
branches.push({ condition, steps })
|
|
1163
|
-
|
|
1164
|
-
// Check for else-if or else
|
|
1165
|
-
if (current.elseStatement) {
|
|
1166
|
-
if (ts.isIfStatement(current.elseStatement)) {
|
|
1167
|
-
// else-if: continue the chain
|
|
1168
|
-
current = current.elseStatement
|
|
1169
|
-
} else {
|
|
1170
|
-
// else: extract the final else block and stop
|
|
1171
|
-
elseSteps = ts.isBlock(current.elseStatement)
|
|
1172
|
-
? extractSteps(current.elseStatement, context, true)
|
|
1173
|
-
: extractStepsFromStatement(current.elseStatement, context)
|
|
1174
|
-
current = undefined
|
|
1175
|
-
}
|
|
1176
|
-
} else {
|
|
1177
|
-
// No else clause
|
|
1178
|
-
current = undefined
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
return {
|
|
1183
|
-
type: 'branch',
|
|
1184
|
-
branches,
|
|
1185
|
-
elseSteps,
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
/**
|
|
1190
|
-
* Extract steps from a single statement (non-block)
|
|
1191
|
-
*/
|
|
1192
|
-
function extractStepsFromStatement(
|
|
1193
|
-
statement: ts.Statement,
|
|
1194
|
-
context: ExtractionContext
|
|
1195
|
-
): WorkflowStepMeta[] {
|
|
1196
|
-
// Increment depth for single-statement blocks (if without braces)
|
|
1197
|
-
context.depth++
|
|
1198
|
-
const step = extractStep(statement, context)
|
|
1199
|
-
context.depth--
|
|
1200
|
-
return step ? [step] : []
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Extract switch statement
|
|
1205
|
-
*/
|
|
1206
|
-
function extractSwitch(
|
|
1207
|
-
statement: ts.SwitchStatement,
|
|
1208
|
-
context: ExtractionContext
|
|
1209
|
-
): SwitchStepMeta | null {
|
|
1210
|
-
const expression = getSourceText(statement.expression)
|
|
1211
|
-
const cases: SwitchCaseMeta[] = []
|
|
1212
|
-
let defaultSteps: WorkflowStepMeta[] | undefined
|
|
1213
|
-
|
|
1214
|
-
for (const clause of statement.caseBlock.clauses) {
|
|
1215
|
-
if (ts.isCaseClause(clause)) {
|
|
1216
|
-
const caseValue = extractCaseValue(clause.expression)
|
|
1217
|
-
const steps = extractCaseSteps(clause.statements, context)
|
|
1218
|
-
|
|
1219
|
-
cases.push({
|
|
1220
|
-
value: caseValue.value,
|
|
1221
|
-
expression: caseValue.expression,
|
|
1222
|
-
steps,
|
|
1223
|
-
})
|
|
1224
|
-
} else if (ts.isDefaultClause(clause)) {
|
|
1225
|
-
defaultSteps = extractCaseSteps(clause.statements, context)
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
return {
|
|
1230
|
-
type: 'switch',
|
|
1231
|
-
expression,
|
|
1232
|
-
cases,
|
|
1233
|
-
defaultSteps,
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
/**
|
|
1238
|
-
* Extract case value from expression
|
|
1239
|
-
*/
|
|
1240
|
-
function extractCaseValue(expr: ts.Expression): {
|
|
1241
|
-
value?: string | number | boolean | null
|
|
1242
|
-
expression?: string
|
|
1243
|
-
} {
|
|
1244
|
-
if (ts.isStringLiteral(expr)) {
|
|
1245
|
-
return { value: expr.text }
|
|
1246
|
-
}
|
|
1247
|
-
if (ts.isNumericLiteral(expr)) {
|
|
1248
|
-
return { value: Number(expr.text) }
|
|
1249
|
-
}
|
|
1250
|
-
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1251
|
-
return { value: true }
|
|
1252
|
-
}
|
|
1253
|
-
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1254
|
-
return { value: false }
|
|
1255
|
-
}
|
|
1256
|
-
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
1257
|
-
return { value: null }
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
return { expression: getSourceText(expr) }
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
/**
|
|
1264
|
-
* Extract steps from case statements, stopping at break
|
|
1265
|
-
*/
|
|
1266
|
-
function extractCaseSteps(
|
|
1267
|
-
statements: ts.NodeArray<ts.Statement>,
|
|
1268
|
-
context: ExtractionContext
|
|
1269
|
-
): WorkflowStepMeta[] {
|
|
1270
|
-
const steps: WorkflowStepMeta[] = []
|
|
1271
|
-
|
|
1272
|
-
// Increment depth for case blocks
|
|
1273
|
-
context.depth++
|
|
1274
|
-
|
|
1275
|
-
for (const statement of statements) {
|
|
1276
|
-
if (ts.isBreakStatement(statement)) {
|
|
1277
|
-
break
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
const step = extractStep(statement, context)
|
|
1281
|
-
if (step) {
|
|
1282
|
-
steps.push(step)
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Restore depth
|
|
1287
|
-
context.depth--
|
|
1288
|
-
|
|
1289
|
-
return steps
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/**
|
|
1293
|
-
* Extract array filter operation
|
|
1294
|
-
*/
|
|
1295
|
-
function extractArrayFilter(
|
|
1296
|
-
call: ts.CallExpression,
|
|
1297
|
-
context: ExtractionContext,
|
|
1298
|
-
outputVar?: string
|
|
1299
|
-
): FilterStepMeta | null {
|
|
1300
|
-
if (!ts.isPropertyAccessExpression(call.expression)) {
|
|
1301
|
-
return null
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
const sourceExpr = call.expression.expression
|
|
1305
|
-
const sourceVar = extractSourcePath(sourceExpr)
|
|
1306
|
-
|
|
1307
|
-
if (!sourceVar) {
|
|
1308
|
-
return null
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const filterFn = call.arguments[0]
|
|
1312
|
-
if (!filterFn || !ts.isArrowFunction(filterFn)) {
|
|
1313
|
-
return null
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const itemParam = filterFn.parameters[0]
|
|
1317
|
-
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
1318
|
-
return null
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const itemVar = itemParam.name.text
|
|
1322
|
-
|
|
1323
|
-
let condition: Condition
|
|
1324
|
-
if (ts.isBlock(filterFn.body)) {
|
|
1325
|
-
return null
|
|
1326
|
-
} else {
|
|
1327
|
-
condition = parseCondition(filterFn.body)
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
return {
|
|
1331
|
-
type: 'filter',
|
|
1332
|
-
sourceVar,
|
|
1333
|
-
itemVar,
|
|
1334
|
-
condition,
|
|
1335
|
-
outputVar,
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
/**
|
|
1340
|
-
* Extract array predicate operation (some/every)
|
|
1341
|
-
*/
|
|
1342
|
-
function extractArrayPredicate(
|
|
1343
|
-
call: ts.CallExpression,
|
|
1344
|
-
context: ExtractionContext,
|
|
1345
|
-
outputVar?: string
|
|
1346
|
-
): ArrayPredicateStepMeta | null {
|
|
1347
|
-
if (!ts.isPropertyAccessExpression(call.expression)) {
|
|
1348
|
-
return null
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
const mode = call.expression.name.text as 'some' | 'every'
|
|
1352
|
-
const sourceExpr = call.expression.expression
|
|
1353
|
-
const sourceVar = extractSourcePath(sourceExpr)
|
|
1354
|
-
|
|
1355
|
-
if (!sourceVar) {
|
|
1356
|
-
return null
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const predicateFn = call.arguments[0]
|
|
1360
|
-
if (!predicateFn || !ts.isArrowFunction(predicateFn)) {
|
|
1361
|
-
return null
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
const itemParam = predicateFn.parameters[0]
|
|
1365
|
-
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
1366
|
-
return null
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
const itemVar = itemParam.name.text
|
|
1370
|
-
|
|
1371
|
-
let condition: Condition
|
|
1372
|
-
if (ts.isBlock(predicateFn.body)) {
|
|
1373
|
-
return null
|
|
1374
|
-
} else {
|
|
1375
|
-
condition = parseCondition(predicateFn.body)
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
return {
|
|
1379
|
-
type: 'arrayPredicate',
|
|
1380
|
-
mode,
|
|
1381
|
-
sourceVar,
|
|
1382
|
-
itemVar,
|
|
1383
|
-
condition,
|
|
1384
|
-
outputVar,
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
/**
|
|
1389
|
-
* Extract an ordered list of workflow.do steps from a per-iteration body.
|
|
1390
|
-
*
|
|
1391
|
-
* Unlike if/switch blocks, a fanout body is a real per-iteration scope, so
|
|
1392
|
-
* `const x = await workflow.do(...)` is meaningful here: the binding is
|
|
1393
|
-
* registered on the child context so later steps in the same iteration can
|
|
1394
|
-
* reference it.
|
|
1395
|
-
*/
|
|
1396
|
-
function extractFanoutBodyStep(
|
|
1397
|
-
stmt: ts.Statement,
|
|
1398
|
-
childContext: ExtractionContext,
|
|
1399
|
-
body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta | ScenarioStepMeta>
|
|
1400
|
-
): void {
|
|
1401
|
-
if (ts.isVariableStatement(stmt)) {
|
|
1402
|
-
const declList = stmt.declarationList
|
|
1403
|
-
if (declList.declarations.length !== 1) {
|
|
1404
|
-
return
|
|
1405
|
-
}
|
|
1406
|
-
const decl = declList.declarations[0]
|
|
1407
|
-
const init = decl.initializer
|
|
1408
|
-
if (
|
|
1409
|
-
!init ||
|
|
1410
|
-
!ts.isAwaitExpression(init) ||
|
|
1411
|
-
!ts.isCallExpression(init.expression)
|
|
1412
|
-
) {
|
|
1413
|
-
return
|
|
1414
|
-
}
|
|
1415
|
-
const call = init.expression
|
|
1416
|
-
if (
|
|
1417
|
-
!isWorkflowDoCall(call, childContext.checker) &&
|
|
1418
|
-
!isScenarioStepCall(call)
|
|
1419
|
-
) {
|
|
1420
|
-
return
|
|
1421
|
-
}
|
|
1422
|
-
const varName = ts.isIdentifier(decl.name) ? decl.name.text : undefined
|
|
1423
|
-
const step = isScenarioStepCall(call)
|
|
1424
|
-
? extractScenarioStep(call, childContext, varName)
|
|
1425
|
-
: extractRpcStep(call, childContext, varName)
|
|
1426
|
-
if (!step) {
|
|
1427
|
-
return
|
|
1428
|
-
}
|
|
1429
|
-
if (varName) {
|
|
1430
|
-
const type = childContext.checker.getTypeAtLocation(decl)
|
|
1431
|
-
childContext.outputVars.set(varName, { type, node: decl })
|
|
1432
|
-
if (isArrayType(type, childContext.checker)) {
|
|
1433
|
-
childContext.arrayVars.add(varName)
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
body.push(step)
|
|
1437
|
-
return
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (ts.isExpressionStatement(stmt) || ts.isReturnStatement(stmt)) {
|
|
1441
|
-
const expr = ts.isReturnStatement(stmt) ? stmt.expression : stmt.expression
|
|
1442
|
-
if (!expr) {
|
|
1443
|
-
return
|
|
1444
|
-
}
|
|
1445
|
-
const call = ts.isAwaitExpression(expr)
|
|
1446
|
-
? ts.isCallExpression(expr.expression)
|
|
1447
|
-
? expr.expression
|
|
1448
|
-
: null
|
|
1449
|
-
: ts.isCallExpression(expr)
|
|
1450
|
-
? expr
|
|
1451
|
-
: null
|
|
1452
|
-
if (!call) {
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
1455
|
-
if (isWorkflowSleepCall(call, childContext.checker)) {
|
|
1456
|
-
const sleepStep = extractSleepStep(call, childContext)
|
|
1457
|
-
if (sleepStep && sleepStep.type === 'sleep') {
|
|
1458
|
-
body.push(sleepStep)
|
|
1459
|
-
}
|
|
1460
|
-
return
|
|
1461
|
-
}
|
|
1462
|
-
if (isWorkflowSuspendCall(call, childContext.checker)) {
|
|
1463
|
-
const suspendStep = extractSuspendStep(call, childContext)
|
|
1464
|
-
if (suspendStep && suspendStep.type === 'suspend') {
|
|
1465
|
-
body.push(suspendStep)
|
|
1466
|
-
}
|
|
1467
|
-
return
|
|
1468
|
-
}
|
|
1469
|
-
if (isScenarioStepCall(call)) {
|
|
1470
|
-
const scenarioStep = extractScenarioStep(call, childContext)
|
|
1471
|
-
if (scenarioStep) {
|
|
1472
|
-
body.push(scenarioStep)
|
|
1473
|
-
}
|
|
1474
|
-
return
|
|
1475
|
-
}
|
|
1476
|
-
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
1477
|
-
return
|
|
1478
|
-
}
|
|
1479
|
-
const step = extractRpcStep(call, childContext)
|
|
1480
|
-
if (step) {
|
|
1481
|
-
body.push(step)
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
/**
|
|
1487
|
-
* Extract parallel fanout from Promise.all(array.map(...))
|
|
1488
|
-
*/
|
|
1489
|
-
function extractParallelFanout(
|
|
1490
|
-
call: ts.CallExpression,
|
|
1491
|
-
context: ExtractionContext
|
|
1492
|
-
): FanoutStepMeta | null {
|
|
1493
|
-
const mapCall = call.arguments[0]
|
|
1494
|
-
if (!ts.isCallExpression(mapCall)) {
|
|
1495
|
-
return null
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
if (!ts.isPropertyAccessExpression(mapCall.expression)) {
|
|
1499
|
-
return null
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
// Extract source array
|
|
1503
|
-
const sourceExpr = mapCall.expression.expression
|
|
1504
|
-
const sourceVar = extractSourcePath(sourceExpr)
|
|
1505
|
-
|
|
1506
|
-
if (!sourceVar) {
|
|
1507
|
-
return null
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Extract map function
|
|
1511
|
-
const mapFn = mapCall.arguments[0]
|
|
1512
|
-
if (!ts.isArrowFunction(mapFn)) {
|
|
1513
|
-
return null
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// Extract item variable
|
|
1517
|
-
const itemParam = mapFn.parameters[0]
|
|
1518
|
-
if (!itemParam || !ts.isIdentifier(itemParam.name)) {
|
|
1519
|
-
return null
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
const itemVar = itemParam.name.text
|
|
1523
|
-
|
|
1524
|
-
// Create a temporary context for the child steps with the loop variable
|
|
1525
|
-
const childContext: ExtractionContext = {
|
|
1526
|
-
...context,
|
|
1527
|
-
outputVars: new Map(context.outputVars),
|
|
1528
|
-
arrayVars: new Set(context.arrayVars),
|
|
1529
|
-
loopVars: new Set([...context.loopVars, itemVar]),
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
const body: FanoutStepMeta['body'] = []
|
|
1533
|
-
|
|
1534
|
-
if (ts.isBlock(mapFn.body)) {
|
|
1535
|
-
for (const stmt of mapFn.body.statements) {
|
|
1536
|
-
extractFanoutBodyStep(stmt, childContext, body)
|
|
1537
|
-
}
|
|
1538
|
-
} else {
|
|
1539
|
-
// Concise body: (item) => workflow.do(...) / async (item) => await workflow.do(...)
|
|
1540
|
-
const expr = mapFn.body
|
|
1541
|
-
const doCall = ts.isAwaitExpression(expr)
|
|
1542
|
-
? ts.isCallExpression(expr.expression)
|
|
1543
|
-
? expr.expression
|
|
1544
|
-
: null
|
|
1545
|
-
: ts.isCallExpression(expr)
|
|
1546
|
-
? expr
|
|
1547
|
-
: null
|
|
1548
|
-
|
|
1549
|
-
if (doCall && isScenarioStepCall(doCall)) {
|
|
1550
|
-
const step = extractScenarioStep(doCall, childContext)
|
|
1551
|
-
if (step) {
|
|
1552
|
-
body.push(step)
|
|
1553
|
-
}
|
|
1554
|
-
} else if (doCall && isWorkflowDoCall(doCall, context.checker)) {
|
|
1555
|
-
const step = extractRpcStep(doCall, childContext)
|
|
1556
|
-
if (step) {
|
|
1557
|
-
body.push(step)
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
if (body.length === 0) {
|
|
1563
|
-
return null
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
return {
|
|
1567
|
-
type: 'fanout',
|
|
1568
|
-
sourceVar,
|
|
1569
|
-
itemVar,
|
|
1570
|
-
mode: 'parallel',
|
|
1571
|
-
body,
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
/**
|
|
1576
|
-
* Extract parallel group from Promise.all([...])
|
|
1577
|
-
*/
|
|
1578
|
-
function extractParallelGroup(
|
|
1579
|
-
call: ts.CallExpression,
|
|
1580
|
-
context: ExtractionContext
|
|
1581
|
-
): ParallelGroupStepMeta | null {
|
|
1582
|
-
const arrayArg = call.arguments[0]
|
|
1583
|
-
if (!ts.isArrayLiteralExpression(arrayArg)) {
|
|
1584
|
-
return null
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
const children: Array<RpcStepMeta | ScenarioStepMeta> = []
|
|
1588
|
-
|
|
1589
|
-
for (const elem of arrayArg.elements) {
|
|
1590
|
-
if (!ts.isCallExpression(elem)) {
|
|
1591
|
-
continue
|
|
1592
|
-
}
|
|
1593
|
-
if (isScenarioStepCall(elem)) {
|
|
1594
|
-
const step = extractScenarioStep(elem, context)
|
|
1595
|
-
if (step) {
|
|
1596
|
-
children.push(step)
|
|
1597
|
-
}
|
|
1598
|
-
} else if (isWorkflowDoCall(elem, context.checker)) {
|
|
1599
|
-
const step = extractRpcStep(elem, context)
|
|
1600
|
-
if (step) {
|
|
1601
|
-
children.push(step)
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (children.length === 0) {
|
|
1607
|
-
return null
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
return {
|
|
1611
|
-
type: 'parallel',
|
|
1612
|
-
children,
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
/**
|
|
1617
|
-
* Extract sequential fanout from for-of loop
|
|
1618
|
-
*/
|
|
1619
|
-
/**
|
|
1620
|
-
* Does this subtree call the workflow wire at all?
|
|
1621
|
-
*
|
|
1622
|
-
* Used to tell "the DSL can't model this loop, and a step would be silently
|
|
1623
|
-
* lost" from "the DSL can't model this loop, and there is nothing in it to
|
|
1624
|
-
* lose". Only the first is worth failing a build over.
|
|
1625
|
-
*/
|
|
1626
|
-
function containsWorkflowCall(node: ts.Node, checker: ts.TypeChecker): boolean {
|
|
1627
|
-
let found = false
|
|
1628
|
-
const visit = (child: ts.Node): void => {
|
|
1629
|
-
if (found) return
|
|
1630
|
-
if (ts.isCallExpression(child)) {
|
|
1631
|
-
if (
|
|
1632
|
-
isWorkflowDoCall(child, checker) ||
|
|
1633
|
-
isScenarioStepCall(child) ||
|
|
1634
|
-
isWorkflowSleepCall(child, checker) ||
|
|
1635
|
-
isWorkflowSuspendCall(child, checker) ||
|
|
1636
|
-
isWorkflowApprovalCall(child, checker) ||
|
|
1637
|
-
isWorkflowExpectEventuallyCall(child)
|
|
1638
|
-
) {
|
|
1639
|
-
found = true
|
|
1640
|
-
return
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
ts.forEachChild(child, visit)
|
|
1644
|
-
}
|
|
1645
|
-
visit(node)
|
|
1646
|
-
return found
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
function extractSequentialFanout(
|
|
1650
|
-
statement: ts.ForOfStatement,
|
|
1651
|
-
context: ExtractionContext,
|
|
1652
|
-
reason?: { message?: string }
|
|
1653
|
-
): FanoutStepMeta | null {
|
|
1654
|
-
if (!isSequentialFanout(statement)) {
|
|
1655
|
-
return null
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
const vars = extractForOfVariable(statement)
|
|
1659
|
-
if (!vars) {
|
|
1660
|
-
return null
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
const { itemVar, sourceVar } = vars
|
|
1664
|
-
|
|
1665
|
-
// Extract child steps and optional sleep from the loop body. A brace-less
|
|
1666
|
-
// body (`for (const x of xs) await workflow.do(...)`) is a single statement
|
|
1667
|
-
// rather than a block, and must still be extracted.
|
|
1668
|
-
const bodyStatements = ts.isBlock(statement.statement)
|
|
1669
|
-
? statement.statement.statements
|
|
1670
|
-
: [statement.statement]
|
|
1671
|
-
|
|
1672
|
-
const body: FanoutStepMeta['body'] = []
|
|
1673
|
-
let timeBetween: string | undefined = undefined
|
|
1674
|
-
|
|
1675
|
-
// Create a child context with the loop variable added
|
|
1676
|
-
const childContext: ExtractionContext = {
|
|
1677
|
-
...context,
|
|
1678
|
-
outputVars: new Map(context.outputVars),
|
|
1679
|
-
arrayVars: new Set(context.arrayVars),
|
|
1680
|
-
loopVars: new Set([...context.loopVars, itemVar]),
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
for (const stmt of bodyStatements) {
|
|
1684
|
-
extractFanoutBodyStep(stmt, childContext, body)
|
|
1685
|
-
|
|
1686
|
-
// Look for workflow.sleep in ExpressionStatement
|
|
1687
|
-
if (ts.isExpressionStatement(stmt)) {
|
|
1688
|
-
const expr = stmt.expression
|
|
1689
|
-
|
|
1690
|
-
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
1691
|
-
const call = expr.expression
|
|
1692
|
-
|
|
1693
|
-
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1694
|
-
// Extract duration for timeBetween
|
|
1695
|
-
const args = call.arguments
|
|
1696
|
-
if (args.length >= 2) {
|
|
1697
|
-
try {
|
|
1698
|
-
const numValue = extractNumberLiteral(args[1])
|
|
1699
|
-
if (numValue !== null) {
|
|
1700
|
-
timeBetween = `${numValue}ms`
|
|
1701
|
-
} else {
|
|
1702
|
-
timeBetween = extractStringLiteral(args[1], context.checker)
|
|
1703
|
-
}
|
|
1704
|
-
} catch {
|
|
1705
|
-
// Ignore extraction errors
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
// Look for if statement with sleep
|
|
1713
|
-
if (ts.isIfStatement(stmt)) {
|
|
1714
|
-
if (ts.isBlock(stmt.thenStatement)) {
|
|
1715
|
-
for (const thenStmt of stmt.thenStatement.statements) {
|
|
1716
|
-
if (ts.isExpressionStatement(thenStmt)) {
|
|
1717
|
-
const expr = thenStmt.expression
|
|
1718
|
-
|
|
1719
|
-
if (
|
|
1720
|
-
ts.isAwaitExpression(expr) &&
|
|
1721
|
-
ts.isCallExpression(expr.expression)
|
|
1722
|
-
) {
|
|
1723
|
-
const call = expr.expression
|
|
1724
|
-
|
|
1725
|
-
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1726
|
-
const args = call.arguments
|
|
1727
|
-
if (args.length >= 2) {
|
|
1728
|
-
try {
|
|
1729
|
-
const numValue = extractNumberLiteral(args[1])
|
|
1730
|
-
if (numValue !== null) {
|
|
1731
|
-
timeBetween = `${numValue}ms`
|
|
1732
|
-
} else {
|
|
1733
|
-
timeBetween = extractStringLiteral(
|
|
1734
|
-
args[1],
|
|
1735
|
-
context.checker
|
|
1736
|
-
)
|
|
1737
|
-
}
|
|
1738
|
-
} catch {
|
|
1739
|
-
// Ignore extraction errors
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
if (body.length === 0) {
|
|
1751
|
-
// The iterable was fine — the body yielded nothing the DSL can model. The
|
|
1752
|
-
// usual cause is a `workflow.do` nested inside an `if`/`switch`, because
|
|
1753
|
-
// `FanoutStepMeta['body']` is a flat list of steps with no branch member:
|
|
1754
|
-
// the DSL has no way to say "conditionally run this step per item".
|
|
1755
|
-
const branching = bodyStatements.some(
|
|
1756
|
-
(stmt) => ts.isIfStatement(stmt) || ts.isSwitchStatement(stmt)
|
|
1757
|
-
)
|
|
1758
|
-
if (reason) {
|
|
1759
|
-
reason.message = branching
|
|
1760
|
-
? `its body only calls 'workflow.do' inside an if/switch, and a DSL fanout body is a flat list of steps with no branch member — the DSL cannot express a per-item condition. Use pikkuWorkflowComplexFunc, or lift the condition out of the loop.`
|
|
1761
|
-
: `its body contains no 'workflow.do' call the DSL can model. Use pikkuWorkflowComplexFunc for a loop that does other work.`
|
|
1762
|
-
}
|
|
1763
|
-
return null
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
return {
|
|
1767
|
-
type: 'fanout',
|
|
1768
|
-
sourceVar,
|
|
1769
|
-
itemVar,
|
|
1770
|
-
mode: 'sequential',
|
|
1771
|
-
body,
|
|
1772
|
-
timeBetween,
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
/**
|
|
1777
|
-
* Extract a single output binding from an expression
|
|
1778
|
-
*/
|
|
1779
|
-
function extractOutputBinding(
|
|
1780
|
-
expr: ts.Expression,
|
|
1781
|
-
context: ExtractionContext
|
|
1782
|
-
): OutputBinding | null {
|
|
1783
|
-
// Check for property access (e.g., org.id, payment.status)
|
|
1784
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
1785
|
-
const objName = ts.isIdentifier(expr.expression)
|
|
1786
|
-
? expr.expression.text
|
|
1787
|
-
: null
|
|
1788
|
-
const propPath = expr.name.text
|
|
1789
|
-
|
|
1790
|
-
if (objName && context.outputVars.has(objName)) {
|
|
1791
|
-
return { from: 'outputVar', name: objName, path: propPath }
|
|
1792
|
-
}
|
|
1793
|
-
if (objName && context.contextVars.has(objName)) {
|
|
1794
|
-
return { from: 'stateVar', name: objName, path: propPath }
|
|
1795
|
-
}
|
|
1796
|
-
if (objName === context.inputParamName) {
|
|
1797
|
-
return { from: 'input', path: propPath }
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// Check for identifier (simple variable reference)
|
|
1802
|
-
if (ts.isIdentifier(expr)) {
|
|
1803
|
-
const varName = expr.text
|
|
1804
|
-
if (context.outputVars.has(varName)) {
|
|
1805
|
-
return { from: 'outputVar', name: varName }
|
|
1806
|
-
}
|
|
1807
|
-
if (context.contextVars.has(varName)) {
|
|
1808
|
-
return { from: 'stateVar', name: varName }
|
|
1809
|
-
}
|
|
1810
|
-
if (varName === context.inputParamName) {
|
|
1811
|
-
return { from: 'input', path: varName }
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// Check for literals
|
|
1816
|
-
if (ts.isStringLiteral(expr)) {
|
|
1817
|
-
return { from: 'literal', value: expr.text }
|
|
1818
|
-
}
|
|
1819
|
-
if (ts.isNumericLiteral(expr)) {
|
|
1820
|
-
return { from: 'literal', value: Number(expr.text) }
|
|
1821
|
-
}
|
|
1822
|
-
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1823
|
-
return { from: 'literal', value: true }
|
|
1824
|
-
}
|
|
1825
|
-
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1826
|
-
return { from: 'literal', value: false }
|
|
1827
|
-
}
|
|
1828
|
-
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
1829
|
-
return { from: 'literal', value: null }
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// For any other expression (comparisons, method calls, etc.), capture as expression
|
|
1833
|
-
return { from: 'expression', expression: getSourceText(expr) }
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
/**
|
|
1837
|
-
* Extract return step
|
|
1838
|
-
*/
|
|
1839
|
-
function extractReturn(
|
|
1840
|
-
statement: ts.ReturnStatement,
|
|
1841
|
-
context: ExtractionContext
|
|
1842
|
-
): WorkflowStepMeta | null {
|
|
1843
|
-
if (!statement.expression) {
|
|
1844
|
-
return null
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
if (
|
|
1848
|
-
ts.isAwaitExpression(statement.expression) &&
|
|
1849
|
-
ts.isCallExpression(statement.expression.expression)
|
|
1850
|
-
) {
|
|
1851
|
-
const call = statement.expression.expression
|
|
1852
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
1853
|
-
return isInlineDoCall(call)
|
|
1854
|
-
? extractInlineStep(call, context)
|
|
1855
|
-
: extractRpcStep(call, context)
|
|
1856
|
-
}
|
|
1857
|
-
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1858
|
-
return extractSleepStep(call, context)
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
if (ts.isCallExpression(statement.expression)) {
|
|
1863
|
-
const call = statement.expression
|
|
1864
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
1865
|
-
return isInlineDoCall(call)
|
|
1866
|
-
? extractInlineStep(call, context)
|
|
1867
|
-
: extractRpcStep(call, context)
|
|
1868
|
-
}
|
|
1869
|
-
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1870
|
-
return extractSleepStep(call, context)
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// `return r` returns every field of r — record it as a spread of one.
|
|
1875
|
-
if (ts.isIdentifier(statement.expression)) {
|
|
1876
|
-
const varName = statement.expression.text
|
|
1877
|
-
if (context.outputVars.has(varName) || context.contextVars.has(varName)) {
|
|
1878
|
-
return { type: 'return', outputs: {}, spread: [varName] }
|
|
1879
|
-
}
|
|
1880
|
-
return null
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1884
|
-
return null
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
const outputs: Record<string, OutputBinding> = {}
|
|
1888
|
-
const spread: string[] = []
|
|
1889
|
-
|
|
1890
|
-
for (const prop of statement.expression.properties) {
|
|
1891
|
-
if (
|
|
1892
|
-
ts.isPropertyAssignment(prop) ||
|
|
1893
|
-
ts.isShorthandPropertyAssignment(prop)
|
|
1894
|
-
) {
|
|
1895
|
-
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null
|
|
1896
|
-
if (!propName) {
|
|
1897
|
-
continue
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
let binding: OutputBinding | null = null
|
|
1901
|
-
|
|
1902
|
-
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1903
|
-
// { orgId } - must be an output variable, context variable, or input
|
|
1904
|
-
const varName = prop.name.text
|
|
1905
|
-
if (context.outputVars.has(varName)) {
|
|
1906
|
-
binding = { from: 'outputVar', name: varName }
|
|
1907
|
-
} else if (context.contextVars.has(varName)) {
|
|
1908
|
-
binding = { from: 'stateVar', name: varName }
|
|
1909
|
-
} else {
|
|
1910
|
-
binding = { from: 'input', path: varName }
|
|
1911
|
-
}
|
|
1912
|
-
} else if (ts.isPropertyAssignment(prop)) {
|
|
1913
|
-
binding = extractOutputBinding(prop.initializer, context)
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
if (binding) {
|
|
1917
|
-
outputs[propName] = binding
|
|
1918
|
-
}
|
|
1919
|
-
} else if (
|
|
1920
|
-
ts.isSpreadAssignment(prop) &&
|
|
1921
|
-
ts.isIdentifier(prop.expression)
|
|
1922
|
-
) {
|
|
1923
|
-
spread.push(prop.expression.text)
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
if (Object.keys(outputs).length === 0 && spread.length === 0) {
|
|
1928
|
-
return null
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
return {
|
|
1932
|
-
type: 'return',
|
|
1933
|
-
outputs,
|
|
1934
|
-
...(spread.length > 0 ? { spread } : {}),
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
/**
|
|
1939
|
-
* Extract input sources from an argument node
|
|
1940
|
-
*/
|
|
1941
|
-
function extractInputSources(
|
|
1942
|
-
node: ts.Node,
|
|
1943
|
-
context: ExtractionContext
|
|
1944
|
-
): Record<string, InputSource> | 'passthrough' | undefined {
|
|
1945
|
-
// Handle when data is passed directly (e.g., workflow.do('step', 'rpc', data))
|
|
1946
|
-
if (ts.isIdentifier(node)) {
|
|
1947
|
-
if (node.text === context.inputParamName) {
|
|
1948
|
-
// The entire input data is being passed through
|
|
1949
|
-
return 'passthrough'
|
|
1950
|
-
}
|
|
1951
|
-
// Check if it's an output variable being passed directly
|
|
1952
|
-
if (context.outputVars.has(node.text)) {
|
|
1953
|
-
return 'passthrough'
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
if (!ts.isObjectLiteralExpression(node)) {
|
|
1958
|
-
return undefined
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
const inputs: Record<string, InputSource> = {}
|
|
1962
|
-
|
|
1963
|
-
for (const prop of node.properties) {
|
|
1964
|
-
if (
|
|
1965
|
-
ts.isPropertyAssignment(prop) ||
|
|
1966
|
-
ts.isShorthandPropertyAssignment(prop)
|
|
1967
|
-
) {
|
|
1968
|
-
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null
|
|
1969
|
-
if (!propName) {
|
|
1970
|
-
continue
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
let source: InputSource | null = null
|
|
1974
|
-
|
|
1975
|
-
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1976
|
-
// { email } - could be from loop var, output var, or input
|
|
1977
|
-
const varName = prop.name.text
|
|
1978
|
-
if (context.loopVars.has(varName)) {
|
|
1979
|
-
source = { from: 'item', path: varName }
|
|
1980
|
-
} else if (context.outputVars.has(varName)) {
|
|
1981
|
-
source = { from: 'outputVar', name: varName }
|
|
1982
|
-
} else {
|
|
1983
|
-
const constant = resolveConstantValue(prop.name, context.checker)
|
|
1984
|
-
source =
|
|
1985
|
-
constant !== undefined
|
|
1986
|
-
? { from: 'literal', value: constant }
|
|
1987
|
-
: { from: 'input', path: varName }
|
|
1988
|
-
}
|
|
1989
|
-
} else if (ts.isPropertyAssignment(prop)) {
|
|
1990
|
-
source = extractInputSource(prop.initializer, context)
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
if (source) {
|
|
1994
|
-
inputs[propName] = source
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
1999
|
-
// Handle spread: { ...data }
|
|
2000
|
-
if (ts.isIdentifier(prop.expression)) {
|
|
2001
|
-
const varName = prop.expression.text
|
|
2002
|
-
if (varName === context.inputParamName) {
|
|
2003
|
-
// This is spreading the input data
|
|
2004
|
-
// We can't fully model this in v1, so we'll skip it
|
|
2005
|
-
continue
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
return Object.keys(inputs).length > 0 ? inputs : undefined
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
function inputSourceToInlineValue(source: InputSource): unknown {
|
|
2015
|
-
switch (source.from) {
|
|
2016
|
-
case 'literal':
|
|
2017
|
-
return source.value
|
|
2018
|
-
case 'input':
|
|
2019
|
-
return { $ref: 'trigger', path: source.path }
|
|
2020
|
-
case 'outputVar':
|
|
2021
|
-
return { $ref: source.name, path: source.path }
|
|
2022
|
-
case 'item':
|
|
2023
|
-
return { $ref: '$item', path: source.path }
|
|
2024
|
-
case 'template':
|
|
2025
|
-
return {
|
|
2026
|
-
$template: {
|
|
2027
|
-
parts: source.parts,
|
|
2028
|
-
expressions: source.expressions.map(inputSourceToInlineValue),
|
|
2029
|
-
},
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
/**
|
|
2035
|
-
* Extract a single input source
|
|
2036
|
-
*/
|
|
2037
|
-
function extractInputSource(
|
|
2038
|
-
node: ts.Node,
|
|
2039
|
-
context: ExtractionContext
|
|
2040
|
-
): InputSource | null {
|
|
2041
|
-
// Property access: data.email, org.id
|
|
2042
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
2043
|
-
const objExpr = node.expression
|
|
2044
|
-
const propName = node.name.text
|
|
2045
|
-
|
|
2046
|
-
if (ts.isIdentifier(objExpr)) {
|
|
2047
|
-
const objName = objExpr.text
|
|
2048
|
-
|
|
2049
|
-
if (objName === context.inputParamName) {
|
|
2050
|
-
return { from: 'input', path: propName }
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
// A field of the fanout item: users.map((u) => do(..., { id: u.id }))
|
|
2054
|
-
if (context.loopVars.has(objName)) {
|
|
2055
|
-
return { from: 'item', path: propName }
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
if (context.outputVars.has(objName)) {
|
|
2059
|
-
return { from: 'outputVar', name: objName, path: propName }
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// Identifier: email, orgId
|
|
2065
|
-
if (ts.isIdentifier(node)) {
|
|
2066
|
-
const varName = node.text
|
|
2067
|
-
|
|
2068
|
-
// Check if it's a loop variable (from fanout)
|
|
2069
|
-
if (context.loopVars.has(varName)) {
|
|
2070
|
-
return { from: 'item', path: varName }
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
if (context.outputVars.has(varName)) {
|
|
2074
|
-
return { from: 'outputVar', name: varName }
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
const constant = resolveConstantValue(node, context.checker)
|
|
2078
|
-
if (constant !== undefined) {
|
|
2079
|
-
return { from: 'literal', value: constant }
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
// Assume it's from input
|
|
2083
|
-
return { from: 'input', path: varName }
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
// Literal: "string", 123, true, false, null
|
|
2087
|
-
if (
|
|
2088
|
-
ts.isStringLiteral(node) ||
|
|
2089
|
-
ts.isNumericLiteral(node) ||
|
|
2090
|
-
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
2091
|
-
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
2092
|
-
node.kind === ts.SyntaxKind.NullKeyword
|
|
2093
|
-
) {
|
|
2094
|
-
let value: unknown
|
|
2095
|
-
if (ts.isStringLiteral(node)) {
|
|
2096
|
-
value = node.text
|
|
2097
|
-
} else if (ts.isNumericLiteral(node)) {
|
|
2098
|
-
value = Number(node.text)
|
|
2099
|
-
} else if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
2100
|
-
value = true
|
|
2101
|
-
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
2102
|
-
value = false
|
|
2103
|
-
} else if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
2104
|
-
value = null
|
|
2105
|
-
}
|
|
2106
|
-
return { from: 'literal', value }
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
// Object literal
|
|
2110
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
2111
|
-
const obj: Record<string, unknown> = {}
|
|
2112
|
-
for (const prop of node.properties) {
|
|
2113
|
-
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
2114
|
-
const propName = prop.name.text
|
|
2115
|
-
const propSource = extractInputSource(prop.initializer, context)
|
|
2116
|
-
if (propSource) {
|
|
2117
|
-
obj[propName] = inputSourceToInlineValue(propSource)
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
return { from: 'literal', value: obj }
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
// Array literal
|
|
2125
|
-
if (ts.isArrayLiteralExpression(node)) {
|
|
2126
|
-
const arr: unknown[] = []
|
|
2127
|
-
for (const elem of node.elements) {
|
|
2128
|
-
const elemSource = extractInputSource(elem, context)
|
|
2129
|
-
if (elemSource) {
|
|
2130
|
-
arr.push(inputSourceToInlineValue(elemSource))
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
return { from: 'literal', value: arr }
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
// No substitution template literal: `hello`
|
|
2137
|
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
2138
|
-
return { from: 'literal', value: node.text }
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
// Template expression with substitutions: `hello ${name}`
|
|
2142
|
-
if (ts.isTemplateExpression(node)) {
|
|
2143
|
-
const parts: string[] = [node.head.text]
|
|
2144
|
-
const expressions: InputSource[] = []
|
|
2145
|
-
|
|
2146
|
-
for (const span of node.templateSpans) {
|
|
2147
|
-
// Extract each expression
|
|
2148
|
-
const exprSource = extractInputSource(span.expression, context)
|
|
2149
|
-
if (exprSource) {
|
|
2150
|
-
expressions.push(exprSource)
|
|
2151
|
-
} else {
|
|
2152
|
-
// Fallback: use source text as literal
|
|
2153
|
-
expressions.push({
|
|
2154
|
-
from: 'literal',
|
|
2155
|
-
value: getSourceText(span.expression),
|
|
2156
|
-
})
|
|
2157
|
-
}
|
|
2158
|
-
parts.push(span.literal.text)
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
return { from: 'template', parts, expressions }
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
return null
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
/**
|
|
2168
|
-
* Resolve an identifier that names a constant to the value it was declared with.
|
|
2169
|
-
*
|
|
2170
|
-
* Without this, a name the extractor does not recognise falls through to
|
|
2171
|
-
* `{ from: 'input' }` and is serialized as `{ $ref: 'trigger', path: <name> }` —
|
|
2172
|
-
* so a module-level `const RESOURCE_ID = 'x'` used as step input silently
|
|
2173
|
-
* becomes a read of a trigger field that does not exist. Only `const`
|
|
2174
|
-
* declarations with a fully literal initializer resolve; anything else (a name
|
|
2175
|
-
* bound out of the trigger, a computed value) falls through unchanged.
|
|
2176
|
-
*/
|
|
2177
|
-
function resolveConstantValue(
|
|
2178
|
-
node: ts.Identifier,
|
|
2179
|
-
checker: ts.TypeChecker
|
|
2180
|
-
): unknown | undefined {
|
|
2181
|
-
const symbol = checker.getSymbolAtLocation(node)
|
|
2182
|
-
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0]
|
|
2183
|
-
if (!declaration || !ts.isVariableDeclaration(declaration)) {
|
|
2184
|
-
return undefined
|
|
2185
|
-
}
|
|
2186
|
-
const declarationList = declaration.parent
|
|
2187
|
-
if (
|
|
2188
|
-
!ts.isVariableDeclarationList(declarationList) ||
|
|
2189
|
-
!(declarationList.flags & ts.NodeFlags.Const)
|
|
2190
|
-
) {
|
|
2191
|
-
return undefined
|
|
2192
|
-
}
|
|
2193
|
-
return declaration.initializer
|
|
2194
|
-
? extractLiteralValue(declaration.initializer)
|
|
2195
|
-
: undefined
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
/**
|
|
2199
|
-
* Extract a literal value from an expression
|
|
2200
|
-
*/
|
|
2201
|
-
function extractLiteralValue(expr: ts.Expression): unknown | undefined {
|
|
2202
|
-
if (ts.isStringLiteral(expr)) {
|
|
2203
|
-
return expr.text
|
|
2204
|
-
}
|
|
2205
|
-
if (ts.isNumericLiteral(expr)) {
|
|
2206
|
-
return Number(expr.text)
|
|
2207
|
-
}
|
|
2208
|
-
if (expr.kind === ts.SyntaxKind.TrueKeyword) {
|
|
2209
|
-
return true
|
|
2210
|
-
}
|
|
2211
|
-
if (expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
2212
|
-
return false
|
|
2213
|
-
}
|
|
2214
|
-
if (expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
2215
|
-
return null
|
|
2216
|
-
}
|
|
2217
|
-
// Array literal
|
|
2218
|
-
if (ts.isArrayLiteralExpression(expr)) {
|
|
2219
|
-
const values: unknown[] = []
|
|
2220
|
-
for (const el of expr.elements) {
|
|
2221
|
-
const v = extractLiteralValue(el)
|
|
2222
|
-
if (v === undefined) return undefined
|
|
2223
|
-
values.push(v)
|
|
2224
|
-
}
|
|
2225
|
-
return values
|
|
2226
|
-
}
|
|
2227
|
-
// Object literal (simple keys with literal values)
|
|
2228
|
-
if (ts.isObjectLiteralExpression(expr)) {
|
|
2229
|
-
const obj: Record<string, unknown> = {}
|
|
2230
|
-
for (const prop of expr.properties) {
|
|
2231
|
-
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
2232
|
-
const v = extractLiteralValue(prop.initializer)
|
|
2233
|
-
if (v === undefined) return undefined
|
|
2234
|
-
obj[prop.name.text] = v
|
|
2235
|
-
} else {
|
|
2236
|
-
return undefined
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
return obj
|
|
2240
|
-
}
|
|
2241
|
-
return undefined
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
/**
|
|
2245
|
-
* Infer a simple type string from a TypeScript type
|
|
2246
|
-
*/
|
|
2247
|
-
function inferSimpleType(type: ts.Type, checker: ts.TypeChecker): string {
|
|
2248
|
-
const typeStr = checker.typeToString(type)
|
|
2249
|
-
if (typeStr === 'string') return 'string'
|
|
2250
|
-
if (typeStr === 'number') return 'number'
|
|
2251
|
-
if (typeStr === 'boolean') return 'boolean'
|
|
2252
|
-
if (typeStr.endsWith('[]') || typeStr.startsWith('Array<')) return 'array'
|
|
2253
|
-
return 'object'
|
|
2254
|
-
}
|