@pikku/inspector 0.12.8 → 0.12.10
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 +27 -0
- package/dist/add/add-functions.js +5 -3
- package/dist/add/add-mcp-prompt.js +4 -0
- package/dist/add/add-mcp-resource.js +4 -0
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +11 -11
- package/dist/utils/extract-function-name.d.ts +1 -0
- package/dist/utils/extract-function-name.js +27 -32
- package/dist/utils/extract-node-value.js +10 -1
- package/dist/utils/filter-inspector-state.js +7 -3
- package/dist/utils/resolve-versions.js +30 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +5 -4
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +47 -28
- package/dist/utils/workflow/dsl/patterns.d.ts +5 -1
- package/dist/utils/workflow/dsl/patterns.js +2 -2
- package/dist/utils/workflow/dsl/validation.d.ts +4 -2
- package/dist/utils/workflow/dsl/validation.js +16 -8
- package/package.json +2 -2
- package/src/add/add-functions.ts +9 -6
- package/src/add/add-mcp-prompt.ts +5 -0
- package/src/add/add-mcp-resource.ts +5 -0
- package/src/add/add-workflow.ts +12 -12
- package/src/utils/extract-function-name.ts +36 -37
- package/src/utils/extract-node-value.test.ts +67 -0
- package/src/utils/extract-node-value.ts +10 -1
- package/src/utils/filter-inspector-state.ts +7 -3
- package/src/utils/resolve-versions.test.ts +141 -0
- package/src/utils/resolve-versions.ts +37 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +58 -32
- package/src/utils/workflow/dsl/patterns.ts +2 -2
- package/src/utils/workflow/dsl/validation.ts +21 -9
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
## 0.12.0
|
|
2
2
|
|
|
3
|
+
## 0.12.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ba8d6ff: Support inline functions in pikkuWorkflowComplexFunc with full DSL extraction
|
|
8
|
+
- d3ace0e: Inspector now captures the `deploy: 'serverless' | 'server' | 'auto'` option
|
|
9
|
+
from `pikkuFunc` / `pikkuSessionlessFunc` calls, alongside the other runtime
|
|
10
|
+
metadata (`expose`, `remote`, `mcp`, `readonly`, `approvalRequired`).
|
|
11
|
+
|
|
12
|
+
Previously this field was defined on `FunctionRuntimeMeta` but never read
|
|
13
|
+
from the user's source, so `deploy: 'server'` was silently dropped. That
|
|
14
|
+
left downstream consumers — notably `@pikku/cli`'s deployment analyzer,
|
|
15
|
+
which routes server-targeted functions to a container unit — treating
|
|
16
|
+
every function as `serverless` regardless of its declared intent.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [311c0c4]
|
|
19
|
+
- @pikku/core@0.12.18
|
|
20
|
+
|
|
21
|
+
## 0.12.9
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 2ac6468: Fix workflow inspector crash when workflow.do() data object has a 'description' property
|
|
26
|
+
- fbcf5b9: Add version awareness to RPC handler: versioned functions now appear in the exposed RPC type map (e.g. `getData@v1`, `getData@v2`), enabling type-safe `rpc.invoke('getData@v1', data)` calls. Tree-shaking respects specific version filters without pulling in all versions. HTTP wirings correctly resolve versioned function IDs.
|
|
27
|
+
- Updated dependencies [fbcf5b9]
|
|
28
|
+
- @pikku/core@0.12.16
|
|
29
|
+
|
|
3
30
|
## 0.12.8
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -245,6 +245,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
245
245
|
let remote;
|
|
246
246
|
let mcp;
|
|
247
247
|
let readonly_;
|
|
248
|
+
let deploy;
|
|
248
249
|
let approvalRequired;
|
|
249
250
|
let approvalDescription;
|
|
250
251
|
let version;
|
|
@@ -312,6 +313,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
312
313
|
remote = getPropertyValue(firstArg, 'remote');
|
|
313
314
|
mcp = getPropertyValue(firstArg, 'mcp');
|
|
314
315
|
readonly_ = getPropertyValue(firstArg, 'readonly');
|
|
316
|
+
deploy = getPropertyValue(firstArg, 'deploy');
|
|
315
317
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
|
|
316
318
|
// Extract approvalDescription identifier reference
|
|
317
319
|
for (const prop of firstArg.properties) {
|
|
@@ -583,6 +585,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
583
585
|
remote: remote || undefined,
|
|
584
586
|
mcp: mcpEnabled || undefined,
|
|
585
587
|
readonly: readonly_ || undefined,
|
|
588
|
+
deploy: deploy || undefined,
|
|
586
589
|
approvalRequired: approvalRequired || undefined,
|
|
587
590
|
approvalDescription: approvalDescription || undefined,
|
|
588
591
|
version,
|
|
@@ -615,15 +618,14 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
615
618
|
}
|
|
616
619
|
if (mcpEnabled) {
|
|
617
620
|
if (!description) {
|
|
618
|
-
logger.
|
|
619
|
-
return;
|
|
621
|
+
logger.warn(`MCP tool '${name}' is missing a description.`);
|
|
620
622
|
}
|
|
621
623
|
state.mcpEndpoints.files.add(node.getSourceFile().fileName);
|
|
622
624
|
state.mcpEndpoints.toolsMeta[name] = {
|
|
623
625
|
pikkuFuncId,
|
|
624
626
|
name,
|
|
625
627
|
title: title || undefined,
|
|
626
|
-
description,
|
|
628
|
+
description: description || undefined,
|
|
627
629
|
summary,
|
|
628
630
|
errors,
|
|
629
631
|
tags,
|
|
@@ -58,6 +58,10 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
|
|
|
58
58
|
}
|
|
59
59
|
const inputSchema = fnMeta.inputs?.[0] || null;
|
|
60
60
|
const outputSchema = fnMeta.outputs?.[0] || null;
|
|
61
|
+
if (!fnMeta.outputSchemaName) {
|
|
62
|
+
fnMeta.outputSchemaName = 'MCPPromptResponse';
|
|
63
|
+
fnMeta.outputs = ['MCPPromptResponse'];
|
|
64
|
+
}
|
|
61
65
|
// --- resolve middleware ---
|
|
62
66
|
const middleware = resolveMiddleware(state, obj, tags, checker);
|
|
63
67
|
// --- resolve permissions ---
|
|
@@ -67,6 +67,10 @@ export const addMCPResource = (logger, node, checker, state, options) => {
|
|
|
67
67
|
}
|
|
68
68
|
const inputSchema = fnMeta.inputs?.[0] || null;
|
|
69
69
|
const outputSchema = fnMeta.outputs?.[0] || null;
|
|
70
|
+
if (!fnMeta.outputSchemaName) {
|
|
71
|
+
fnMeta.outputSchemaName = 'MCPResourceResponse';
|
|
72
|
+
fnMeta.outputs = ['MCPResourceResponse'];
|
|
73
|
+
}
|
|
70
74
|
// --- resolve middleware ---
|
|
71
75
|
const middleware = resolveMiddleware(state, obj, tags, checker);
|
|
72
76
|
// --- resolve permissions ---
|
|
@@ -5,7 +5,7 @@ import type { WorkflowStepMeta } from '@pikku/core/workflow';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
|
|
7
7
|
/**
|
|
8
|
-
* Inspector for
|
|
8
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
9
9
|
* Detects workflow registration and extracts metadata
|
|
10
10
|
*/
|
|
11
11
|
export declare const addWorkflow: AddWiring;
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -88,7 +88,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
88
88
|
// workflow.do(stepName, rpcName|fn, data?, options?)
|
|
89
89
|
const stepNameArg = args[0];
|
|
90
90
|
const secondArg = args[1];
|
|
91
|
-
const optionsArg = args.length >=
|
|
91
|
+
const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
|
|
92
92
|
const stepName = extractStringLiteral(stepNameArg, checker);
|
|
93
93
|
const description = extractDescription(optionsArg, checker) ?? undefined;
|
|
94
94
|
// Determine form by checking 2nd argument type
|
|
@@ -142,7 +142,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
* Inspector for
|
|
145
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
146
146
|
* Detects workflow registration and extracts metadata
|
|
147
147
|
*/
|
|
148
148
|
export const addWorkflow = (logger, node, checker, state) => {
|
|
@@ -160,7 +160,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
160
160
|
wrapperType = 'dsl';
|
|
161
161
|
}
|
|
162
162
|
else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
163
|
-
wrapperType = '
|
|
163
|
+
wrapperType = 'complex';
|
|
164
164
|
}
|
|
165
165
|
else {
|
|
166
166
|
return;
|
|
@@ -220,7 +220,9 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
220
220
|
let dsl = undefined;
|
|
221
221
|
// Try DSL workflow extraction first
|
|
222
222
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
223
|
-
const result = extractDSLWorkflow(node, checker
|
|
223
|
+
const result = extractDSLWorkflow(node, checker, {
|
|
224
|
+
allowInline: wrapperType === 'complex',
|
|
225
|
+
});
|
|
224
226
|
if (result.status === 'ok' && result.steps) {
|
|
225
227
|
// Extraction succeeded
|
|
226
228
|
steps = result.steps;
|
|
@@ -252,7 +254,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
252
254
|
// For pikkuWorkflowFunc, this is a critical error
|
|
253
255
|
// But still track RPC invocations for function registration
|
|
254
256
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
|
|
255
|
-
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}'
|
|
257
|
+
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
else {
|
|
@@ -261,12 +263,10 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
261
263
|
dsl = false;
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*/
|
|
269
|
-
if (!dsl || wrapperType === 'regular') {
|
|
266
|
+
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
267
|
+
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
268
|
+
// with identifier args, etc.) are still registered as invoked functions.
|
|
269
|
+
if (wrapperType === 'complex') {
|
|
270
270
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
|
|
271
271
|
}
|
|
272
272
|
state.workflows.meta[workflowName] = {
|
|
@@ -6,6 +6,7 @@ export type ExtractedFunctionName = {
|
|
|
6
6
|
exportedName: string | null;
|
|
7
7
|
propertyName: string | null;
|
|
8
8
|
isHelper: boolean;
|
|
9
|
+
version: number | null;
|
|
9
10
|
};
|
|
10
11
|
export declare function makeContextBasedId(wiringType: string, ...segments: string[]): string;
|
|
11
12
|
export declare function funcIdToTypeName(id: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
+
import { formatVersionedId } from '@pikku/core';
|
|
3
4
|
export function makeContextBasedId(wiringType, ...segments) {
|
|
4
5
|
return [wiringType, ...segments].join(':');
|
|
5
6
|
}
|
|
@@ -20,6 +21,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
20
21
|
propertyName: null,
|
|
21
22
|
explicitName: null,
|
|
22
23
|
isHelper: false,
|
|
24
|
+
version: null,
|
|
23
25
|
};
|
|
24
26
|
const workflowHelpers = new Set([
|
|
25
27
|
'workflow',
|
|
@@ -103,16 +105,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
103
105
|
// Check for object with 'name' property in first argument
|
|
104
106
|
const firstArg = args[0];
|
|
105
107
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
106
|
-
|
|
107
|
-
if (ts.isPropertyAssignment(prop) &&
|
|
108
|
-
ts.isIdentifier(prop.name) &&
|
|
109
|
-
prop.name.text === 'override' &&
|
|
110
|
-
ts.isStringLiteral(prop.initializer)) {
|
|
111
|
-
// Priority 1: Object with override property
|
|
112
|
-
result.explicitName = prop.initializer.text;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
108
|
+
extractOverrideAndVersion(firstArg, result);
|
|
116
109
|
}
|
|
117
110
|
// Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
|
|
118
111
|
if (expression.text.startsWith('pikku')) {
|
|
@@ -274,19 +267,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
274
267
|
if (ts.isCallExpression(decl.initializer) &&
|
|
275
268
|
ts.isIdentifier(decl.initializer.expression) &&
|
|
276
269
|
decl.initializer.expression.text.startsWith('pikku')) {
|
|
277
|
-
// Check for object with 'override' property in first argument
|
|
278
270
|
const firstArg = decl.initializer.arguments[0];
|
|
279
271
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
280
|
-
|
|
281
|
-
if (ts.isPropertyAssignment(prop) &&
|
|
282
|
-
ts.isIdentifier(prop.name) &&
|
|
283
|
-
prop.name.text === 'override' &&
|
|
284
|
-
ts.isStringLiteral(prop.initializer)) {
|
|
285
|
-
// Priority 1: Object with override property
|
|
286
|
-
result.explicitName = prop.initializer.text;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
272
|
+
extractOverrideAndVersion(firstArg, result);
|
|
290
273
|
}
|
|
291
274
|
if (decl.initializer.expression.text.startsWith('pikku')) {
|
|
292
275
|
if (firstArg &&
|
|
@@ -340,17 +323,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
340
323
|
else if (ts.isCallExpression(callExpr)) {
|
|
341
324
|
const firstArg = callExpr.arguments[0];
|
|
342
325
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
343
|
-
|
|
344
|
-
if (ts.isPropertyAssignment(prop) &&
|
|
345
|
-
ts.isIdentifier(prop.name) &&
|
|
346
|
-
prop.name.text === 'override' &&
|
|
347
|
-
ts.isStringLiteral(prop.initializer) &&
|
|
348
|
-
!result.explicitName // Only set if not already set
|
|
349
|
-
) {
|
|
350
|
-
result.explicitName = prop.initializer.text;
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
326
|
+
extractOverrideAndVersion(firstArg, result);
|
|
354
327
|
}
|
|
355
328
|
}
|
|
356
329
|
// Apply name priority logic
|
|
@@ -364,6 +337,9 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
364
337
|
else {
|
|
365
338
|
result.pikkuFuncId = `__temp_${randomUUID()}`;
|
|
366
339
|
}
|
|
340
|
+
if (result.version !== null) {
|
|
341
|
+
result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version);
|
|
342
|
+
}
|
|
367
343
|
return result;
|
|
368
344
|
}
|
|
369
345
|
/**
|
|
@@ -420,3 +396,22 @@ export function isNamedExport(declaration, checker) {
|
|
|
420
396
|
}
|
|
421
397
|
return false;
|
|
422
398
|
}
|
|
399
|
+
function extractOverrideAndVersion(objLiteral, result) {
|
|
400
|
+
for (const prop of objLiteral.properties) {
|
|
401
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
402
|
+
if (prop.name.text === 'override' &&
|
|
403
|
+
ts.isStringLiteral(prop.initializer) &&
|
|
404
|
+
!result.explicitName) {
|
|
405
|
+
result.explicitName = prop.initializer.text;
|
|
406
|
+
}
|
|
407
|
+
else if (prop.name.text === 'version' &&
|
|
408
|
+
ts.isNumericLiteral(prop.initializer) &&
|
|
409
|
+
result.version === null) {
|
|
410
|
+
const parsed = Number(prop.initializer.text);
|
|
411
|
+
if (Number.isInteger(parsed) && parsed >= 1) {
|
|
412
|
+
result.version = parsed;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -20,6 +20,10 @@ export function extractStringLiteral(node, checker) {
|
|
|
20
20
|
}
|
|
21
21
|
return result;
|
|
22
22
|
}
|
|
23
|
+
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
24
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
25
|
+
return extractStringLiteral(node.expression, checker);
|
|
26
|
+
}
|
|
23
27
|
// Try to evaluate constant identifiers
|
|
24
28
|
if (ts.isIdentifier(node)) {
|
|
25
29
|
const symbol = checker.getSymbolAtLocation(node);
|
|
@@ -84,7 +88,12 @@ export function extractDescription(optionsNode, checker) {
|
|
|
84
88
|
if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
|
|
85
89
|
return null;
|
|
86
90
|
}
|
|
87
|
-
|
|
91
|
+
try {
|
|
92
|
+
return extractPropertyString(optionsNode, 'description', checker);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
/**
|
|
90
99
|
* Extract duration value (number or string)
|
|
@@ -532,11 +532,15 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
532
532
|
filteredState.serviceAggregation.usedFunctions.add(funcId);
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
|
-
// Post-filter version expansion:
|
|
535
|
+
// Post-filter version expansion: when an unversioned base name is matched,
|
|
536
|
+
// include all its versions. Specific version matches (e.g. analyzeData@v1)
|
|
537
|
+
// do NOT expand to include other versions.
|
|
536
538
|
const includedBaseNames = new Set();
|
|
537
539
|
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
538
|
-
const { baseName } = parseVersionedId(funcId);
|
|
539
|
-
|
|
540
|
+
const { baseName, version } = parseVersionedId(funcId);
|
|
541
|
+
if (version === null) {
|
|
542
|
+
includedBaseNames.add(baseName);
|
|
543
|
+
}
|
|
540
544
|
}
|
|
541
545
|
if (includedBaseNames.size > 0) {
|
|
542
546
|
for (const funcId of Object.keys(state.functions.meta)) {
|
|
@@ -66,13 +66,43 @@ export function resolveLatestVersions(state, logger) {
|
|
|
66
66
|
if (state.rpc.exposedMeta[baseName] === oldId) {
|
|
67
67
|
state.rpc.exposedMeta[baseName] = newId;
|
|
68
68
|
}
|
|
69
|
+
updateWiringReferences(state, oldId, newId);
|
|
69
70
|
}
|
|
70
71
|
else {
|
|
71
72
|
const latest = group.explicit.reduce((a, b) => a.version > b.version ? a : b);
|
|
72
73
|
state.rpc.internalMeta[baseName] = latest.id;
|
|
73
74
|
}
|
|
75
|
+
if (state.rpc.exposedMeta[baseName]) {
|
|
76
|
+
const latestId = state.rpc.internalMeta[baseName];
|
|
77
|
+
state.rpc.exposedMeta[baseName] = latestId;
|
|
78
|
+
for (const entry of group.explicit) {
|
|
79
|
+
state.rpc.exposedMeta[entry.id] = entry.id;
|
|
80
|
+
const fileEntry = state.rpc.internalFiles.get(entry.id);
|
|
81
|
+
if (fileEntry) {
|
|
82
|
+
state.rpc.exposedFiles.set(entry.id, fileEntry);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (group.unversioned) {
|
|
86
|
+
state.rpc.exposedMeta[latestId] = latestId;
|
|
87
|
+
const fileEntry = state.rpc.internalFiles.get(latestId);
|
|
88
|
+
if (fileEntry) {
|
|
89
|
+
state.rpc.exposedFiles.set(latestId, fileEntry);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
74
93
|
for (const entry of group.explicit) {
|
|
75
94
|
state.rpc.invokedFunctions.add(entry.id);
|
|
76
95
|
}
|
|
77
96
|
}
|
|
78
97
|
}
|
|
98
|
+
function updateWiringReferences(state, oldId, newId) {
|
|
99
|
+
if (state.http) {
|
|
100
|
+
for (const methods of Object.values(state.http.meta)) {
|
|
101
|
+
for (const meta of Object.values(methods)) {
|
|
102
|
+
if (meta.pikkuFuncId === oldId) {
|
|
103
|
+
meta.pikkuFuncId = newId;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow';
|
|
3
3
|
/**
|
|
4
|
-
* Result of
|
|
4
|
+
* Result of DSL workflow extraction
|
|
5
5
|
*/
|
|
6
6
|
export interface ExtractionResult {
|
|
7
7
|
status: 'ok' | 'error';
|
|
@@ -9,9 +9,10 @@ export interface ExtractionResult {
|
|
|
9
9
|
/** Workflow context (top-level variables) */
|
|
10
10
|
context?: WorkflowContext;
|
|
11
11
|
reason?: string;
|
|
12
|
-
simple?: boolean;
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
|
-
* Extract
|
|
14
|
+
* Extract DSL workflow metadata from a function declaration
|
|
16
15
|
*/
|
|
17
|
-
export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker
|
|
16
|
+
export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker, options?: {
|
|
17
|
+
allowInline?: boolean;
|
|
18
|
+
}): ExtractionResult;
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
6
|
-
* Extract
|
|
6
|
+
* Extract DSL workflow metadata from a function declaration
|
|
7
7
|
*/
|
|
8
|
-
function
|
|
9
|
-
if (ts.isIdentifier(expr)) {
|
|
10
|
-
return expr.text;
|
|
11
|
-
}
|
|
12
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
13
|
-
const base = extractSourcePath(expr.expression);
|
|
14
|
-
if (base) {
|
|
15
|
-
return `${base}.${expr.name.text}`;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Extract simple workflow metadata from a function declaration
|
|
22
|
-
*/
|
|
23
|
-
export function extractDSLWorkflow(funcNode, checker) {
|
|
8
|
+
export function extractDSLWorkflow(funcNode, checker, options) {
|
|
24
9
|
try {
|
|
25
10
|
// Find the async arrow function
|
|
26
11
|
const arrowFunc = findWorkflowFunction(funcNode);
|
|
@@ -28,7 +13,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
28
13
|
return {
|
|
29
14
|
status: 'error',
|
|
30
15
|
reason: 'Could not find async arrow function in workflow definition',
|
|
31
|
-
simple: false,
|
|
32
16
|
};
|
|
33
17
|
}
|
|
34
18
|
// Extract input parameter name (second parameter)
|
|
@@ -37,7 +21,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
37
21
|
return {
|
|
38
22
|
status: 'error',
|
|
39
23
|
reason: 'Could not determine input parameter name',
|
|
40
|
-
simple: false,
|
|
41
24
|
};
|
|
42
25
|
}
|
|
43
26
|
// Initialize extraction context
|
|
@@ -53,12 +36,13 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
53
36
|
depth: 0,
|
|
54
37
|
};
|
|
55
38
|
// Validate no disallowed patterns
|
|
56
|
-
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body
|
|
39
|
+
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body, {
|
|
40
|
+
allowInline: options?.allowInline,
|
|
41
|
+
});
|
|
57
42
|
if (patternErrors.length > 0) {
|
|
58
43
|
return {
|
|
59
44
|
status: 'error',
|
|
60
45
|
reason: formatValidationErrors(patternErrors),
|
|
61
|
-
simple: false,
|
|
62
46
|
};
|
|
63
47
|
}
|
|
64
48
|
// Validate all workflow calls are awaited
|
|
@@ -67,7 +51,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
67
51
|
return {
|
|
68
52
|
status: 'error',
|
|
69
53
|
reason: formatValidationErrors(awaitErrors),
|
|
70
|
-
simple: false,
|
|
71
54
|
};
|
|
72
55
|
}
|
|
73
56
|
// Extract steps from function body
|
|
@@ -77,7 +60,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
77
60
|
return {
|
|
78
61
|
status: 'error',
|
|
79
62
|
reason: formatValidationErrors(context.errors),
|
|
80
|
-
simple: false,
|
|
81
63
|
};
|
|
82
64
|
}
|
|
83
65
|
// Build workflow context from extracted context variables
|
|
@@ -92,14 +74,12 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
92
74
|
status: 'ok',
|
|
93
75
|
steps,
|
|
94
76
|
context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
|
|
95
|
-
simple: true,
|
|
96
77
|
};
|
|
97
78
|
}
|
|
98
79
|
catch (error) {
|
|
99
80
|
return {
|
|
100
81
|
status: 'error',
|
|
101
82
|
reason: error instanceof Error ? error.message : String(error),
|
|
102
|
-
simple: false,
|
|
103
83
|
};
|
|
104
84
|
}
|
|
105
85
|
}
|
|
@@ -248,7 +228,9 @@ function extractVariableDeclaration(statement, context) {
|
|
|
248
228
|
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
249
229
|
const call = init.expression;
|
|
250
230
|
if (isWorkflowDoCall(call, context.checker)) {
|
|
251
|
-
const step =
|
|
231
|
+
const step = isInlineDoCall(call)
|
|
232
|
+
? extractInlineStep(call, context)
|
|
233
|
+
: extractRpcStep(call, context, varName);
|
|
252
234
|
if (step) {
|
|
253
235
|
// Track output variable
|
|
254
236
|
const type = context.checker.getTypeAtLocation(decl);
|
|
@@ -335,7 +317,9 @@ function extractExpressionStatement(statement, context) {
|
|
|
335
317
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
336
318
|
const call = expr.expression;
|
|
337
319
|
if (isWorkflowDoCall(call, context.checker)) {
|
|
338
|
-
const step =
|
|
320
|
+
const step = isInlineDoCall(call)
|
|
321
|
+
? extractInlineStep(call, context)
|
|
322
|
+
: extractRpcStep(call, context, outputVar);
|
|
339
323
|
// Track output variable if this is an assignment
|
|
340
324
|
if (outputVar && step) {
|
|
341
325
|
const type = context.checker.getTypeAtLocation(expr);
|
|
@@ -394,6 +378,41 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
394
378
|
return null;
|
|
395
379
|
}
|
|
396
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Extract inline step from workflow.do() call with a function argument
|
|
383
|
+
*/
|
|
384
|
+
function extractInlineStep(call, context) {
|
|
385
|
+
const args = call.arguments;
|
|
386
|
+
if (args.length < 2)
|
|
387
|
+
return null;
|
|
388
|
+
try {
|
|
389
|
+
const stepName = extractStringLiteral(args[0], context.checker);
|
|
390
|
+
const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
|
|
391
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
392
|
+
? extractStepOptions(optionsArg, context)
|
|
393
|
+
: undefined;
|
|
394
|
+
return {
|
|
395
|
+
type: 'inline',
|
|
396
|
+
stepName,
|
|
397
|
+
options,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
context.errors.push({
|
|
402
|
+
message: `Failed to extract inline step: ${error instanceof Error ? error.message : String(error)}`,
|
|
403
|
+
node: call,
|
|
404
|
+
});
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if the second argument of a workflow.do() call is a function
|
|
410
|
+
*/
|
|
411
|
+
function isInlineDoCall(call) {
|
|
412
|
+
const secondArg = call.arguments[1];
|
|
413
|
+
return (!!secondArg &&
|
|
414
|
+
(ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg)));
|
|
415
|
+
}
|
|
397
416
|
/**
|
|
398
417
|
* Extract step options from options object
|
|
399
418
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Pattern detection helpers for
|
|
3
|
+
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do()
|
|
@@ -43,6 +43,10 @@ export declare function isParallelGroup(node: ts.CallExpression): boolean;
|
|
|
43
43
|
* Check if a for statement is a valid sequential fanout (for..of)
|
|
44
44
|
*/
|
|
45
45
|
export declare function isSequentialFanout(node: ts.ForOfStatement): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Extract full source path from an expression (e.g., data.memberEmails)
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractSourcePath(expr: ts.Expression): string | null;
|
|
46
50
|
/**
|
|
47
51
|
* Extract the variable name from a for..of statement
|
|
48
52
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Pattern detection helpers for
|
|
3
|
+
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do()
|
|
@@ -162,7 +162,7 @@ export function isSequentialFanout(node) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Extract full source path from an expression (e.g., data.memberEmails)
|
|
164
164
|
*/
|
|
165
|
-
function extractSourcePath(expr) {
|
|
165
|
+
export function extractSourcePath(expr) {
|
|
166
166
|
if (ts.isIdentifier(expr)) {
|
|
167
167
|
return expr.text;
|
|
168
168
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Validation rules for
|
|
3
|
+
* Validation rules for DSL workflows
|
|
4
4
|
*/
|
|
5
5
|
export interface ValidationError {
|
|
6
6
|
message: string;
|
|
@@ -19,7 +19,9 @@ export interface ValidationError {
|
|
|
19
19
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
20
20
|
* - Block (containers)
|
|
21
21
|
*/
|
|
22
|
-
export declare function validateNoDisallowedPatterns(node: ts.Node
|
|
22
|
+
export declare function validateNoDisallowedPatterns(node: ts.Node, options?: {
|
|
23
|
+
allowInline?: boolean;
|
|
24
|
+
}): ValidationError[];
|
|
23
25
|
/**
|
|
24
26
|
* Validate that all workflow.do calls are awaited
|
|
25
27
|
*/
|