@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
|
@@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
|
|
12
12
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
13
13
|
* - Block (containers)
|
|
14
14
|
*/
|
|
15
|
-
export function validateNoDisallowedPatterns(node) {
|
|
15
|
+
export function validateNoDisallowedPatterns(node, options) {
|
|
16
16
|
const errors = [];
|
|
17
17
|
function visitBlock(block) {
|
|
18
18
|
for (const statement of block.statements) {
|
|
@@ -30,7 +30,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
30
30
|
// Unknown/disallowed statement type
|
|
31
31
|
const nodeType = ts.SyntaxKind[statement.kind];
|
|
32
32
|
errors.push({
|
|
33
|
-
message: `Statement type '${nodeType}' is not allowed in
|
|
33
|
+
message: `Statement type '${nodeType}' is not allowed in DSL workflows. Allowed: const/let, if/else, switch/case, for..of, return, throw, and workflow calls. If this should be supported, please report the node type: ${nodeType}`,
|
|
34
34
|
node: statement,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -40,7 +40,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
40
40
|
// Disallow while and do-while
|
|
41
41
|
if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
|
|
42
42
|
errors.push({
|
|
43
|
-
message: 'while and do-while loops are not allowed in
|
|
43
|
+
message: 'while and do-while loops are not allowed in DSL workflows',
|
|
44
44
|
node,
|
|
45
45
|
});
|
|
46
46
|
return;
|
|
@@ -48,13 +48,13 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
48
48
|
// Disallow for and for-in loops
|
|
49
49
|
if (ts.isForInStatement(node) || ts.isForStatement(node)) {
|
|
50
50
|
errors.push({
|
|
51
|
-
message: 'for and for-in loops are not allowed in
|
|
51
|
+
message: 'for and for-in loops are not allowed in DSL workflows. Use for-of instead.',
|
|
52
52
|
node,
|
|
53
53
|
});
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
// Check for inline workflow.do
|
|
57
|
-
if (ts.isCallExpression(node)) {
|
|
57
|
+
if (!options?.allowInline && ts.isCallExpression(node)) {
|
|
58
58
|
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
59
59
|
const propAccess = node.expression;
|
|
60
60
|
if (propAccess.name.text === 'do' &&
|
|
@@ -65,7 +65,7 @@ export function validateNoDisallowedPatterns(node) {
|
|
|
65
65
|
(ts.isArrowFunction(secondArg) ||
|
|
66
66
|
ts.isFunctionExpression(secondArg))) {
|
|
67
67
|
errors.push({
|
|
68
|
-
message: 'Inline workflow.do with function argument is not allowed in
|
|
68
|
+
message: 'Inline workflow.do with function argument is not allowed in DSL workflows. Use pikkuWorkflowComplexFunc instead.',
|
|
69
69
|
node,
|
|
70
70
|
});
|
|
71
71
|
return;
|
|
@@ -97,11 +97,19 @@ export function validateAwaitedCalls(node) {
|
|
|
97
97
|
if (propAccess.name.text === 'all' &&
|
|
98
98
|
ts.isIdentifier(propAccess.expression) &&
|
|
99
99
|
propAccess.expression.text === 'Promise') {
|
|
100
|
-
// console.log('[DEBUG] Found Promise.all, setting insidePromiseAll=true')
|
|
101
|
-
// Visit children with insidePromiseAll = true
|
|
102
100
|
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
103
101
|
return;
|
|
104
102
|
}
|
|
103
|
+
// .push() on an array — workflow.do() inside is collecting promises
|
|
104
|
+
if (propAccess.name.text === 'push') {
|
|
105
|
+
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Array literal — workflow.do() inside is collecting promises
|
|
110
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
111
|
+
ts.forEachChild(node, (child) => visit(child, parentIsAwait, true));
|
|
112
|
+
return;
|
|
105
113
|
}
|
|
106
114
|
// Now check for workflow calls
|
|
107
115
|
if (ts.isCallExpression(node)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.18",
|
|
39
39
|
"path-to-regexp": "^8.3.0",
|
|
40
40
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
41
|
"tsx": "^4.21.0",
|
package/src/add/add-functions.ts
CHANGED
|
@@ -339,6 +339,7 @@ export const addFunctions: AddWiring = (
|
|
|
339
339
|
let remote: boolean | undefined
|
|
340
340
|
let mcp: boolean | undefined
|
|
341
341
|
let readonly_: boolean | undefined
|
|
342
|
+
let deploy: 'serverless' | 'server' | 'auto' | undefined
|
|
342
343
|
let approvalRequired: boolean | undefined
|
|
343
344
|
let approvalDescription: string | undefined
|
|
344
345
|
let version: number | undefined
|
|
@@ -422,6 +423,11 @@ export const addFunctions: AddWiring = (
|
|
|
422
423
|
remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
|
|
423
424
|
mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
|
|
424
425
|
readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
|
|
426
|
+
deploy = getPropertyValue(firstArg, 'deploy') as
|
|
427
|
+
| 'serverless'
|
|
428
|
+
| 'server'
|
|
429
|
+
| 'auto'
|
|
430
|
+
| undefined
|
|
425
431
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
426
432
|
| boolean
|
|
427
433
|
| undefined
|
|
@@ -783,6 +789,7 @@ export const addFunctions: AddWiring = (
|
|
|
783
789
|
remote: remote || undefined,
|
|
784
790
|
mcp: mcpEnabled || undefined,
|
|
785
791
|
readonly: readonly_ || undefined,
|
|
792
|
+
deploy: deploy || undefined,
|
|
786
793
|
approvalRequired: approvalRequired || undefined,
|
|
787
794
|
approvalDescription: approvalDescription || undefined,
|
|
788
795
|
version,
|
|
@@ -817,18 +824,14 @@ export const addFunctions: AddWiring = (
|
|
|
817
824
|
|
|
818
825
|
if (mcpEnabled) {
|
|
819
826
|
if (!description) {
|
|
820
|
-
logger.
|
|
821
|
-
ErrorCode.MISSING_DESCRIPTION,
|
|
822
|
-
`MCP tool '${name}' is missing a description.`
|
|
823
|
-
)
|
|
824
|
-
return
|
|
827
|
+
logger.warn(`MCP tool '${name}' is missing a description.`)
|
|
825
828
|
}
|
|
826
829
|
state.mcpEndpoints.files.add(node.getSourceFile().fileName)
|
|
827
830
|
state.mcpEndpoints.toolsMeta[name] = {
|
|
828
831
|
pikkuFuncId,
|
|
829
832
|
name,
|
|
830
833
|
title: title || undefined,
|
|
831
|
-
description,
|
|
834
|
+
description: description || undefined,
|
|
832
835
|
summary,
|
|
833
836
|
errors,
|
|
834
837
|
tags,
|
|
@@ -114,6 +114,11 @@ export const addMCPPrompt: AddWiring = (
|
|
|
114
114
|
const inputSchema = fnMeta.inputs?.[0] || null
|
|
115
115
|
const outputSchema = fnMeta.outputs?.[0] || null
|
|
116
116
|
|
|
117
|
+
if (!fnMeta.outputSchemaName) {
|
|
118
|
+
fnMeta.outputSchemaName = 'MCPPromptResponse'
|
|
119
|
+
fnMeta.outputs = ['MCPPromptResponse']
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
// --- resolve middleware ---
|
|
118
123
|
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
119
124
|
|
|
@@ -131,6 +131,11 @@ export const addMCPResource: AddWiring = (
|
|
|
131
131
|
const inputSchema = fnMeta.inputs?.[0] || null
|
|
132
132
|
const outputSchema = fnMeta.outputs?.[0] || null
|
|
133
133
|
|
|
134
|
+
if (!fnMeta.outputSchemaName) {
|
|
135
|
+
fnMeta.outputSchemaName = 'MCPResourceResponse'
|
|
136
|
+
fnMeta.outputs = ['MCPResourceResponse']
|
|
137
|
+
}
|
|
138
|
+
|
|
134
139
|
// --- resolve middleware ---
|
|
135
140
|
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
136
141
|
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -97,7 +97,7 @@ function getWorkflowInvocations(
|
|
|
97
97
|
const stepNameArg = args[0]
|
|
98
98
|
const secondArg = args[1]
|
|
99
99
|
const optionsArg =
|
|
100
|
-
args.length >=
|
|
100
|
+
args.length >= 4 ? args[args.length - 1] : undefined
|
|
101
101
|
|
|
102
102
|
const stepName = extractStringLiteral(stepNameArg, checker)
|
|
103
103
|
const description =
|
|
@@ -155,7 +155,7 @@ function getWorkflowInvocations(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* Inspector for
|
|
158
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
159
159
|
* Detects workflow registration and extracts metadata
|
|
160
160
|
*/
|
|
161
161
|
export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
@@ -171,11 +171,11 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
171
171
|
return
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
let wrapperType: 'dsl' | '
|
|
174
|
+
let wrapperType: 'dsl' | 'complex' | null = null
|
|
175
175
|
if (expression.text === 'pikkuWorkflowFunc') {
|
|
176
176
|
wrapperType = 'dsl'
|
|
177
177
|
} else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
178
|
-
wrapperType = '
|
|
178
|
+
wrapperType = 'complex'
|
|
179
179
|
} else {
|
|
180
180
|
return
|
|
181
181
|
}
|
|
@@ -267,7 +267,9 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
267
267
|
|
|
268
268
|
// Try DSL workflow extraction first
|
|
269
269
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
270
|
-
const result = extractDSLWorkflow(node, checker
|
|
270
|
+
const result = extractDSLWorkflow(node, checker, {
|
|
271
|
+
allowInline: wrapperType === 'complex',
|
|
272
|
+
})
|
|
271
273
|
|
|
272
274
|
if (result.status === 'ok' && result.steps) {
|
|
273
275
|
// Extraction succeeded
|
|
@@ -305,7 +307,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
305
307
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
306
308
|
logger.critical(
|
|
307
309
|
ErrorCode.INVALID_DSL_WORKFLOW,
|
|
308
|
-
`Workflow '${workflowName}'
|
|
310
|
+
`Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`
|
|
309
311
|
)
|
|
310
312
|
return
|
|
311
313
|
} else {
|
|
@@ -317,12 +319,10 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
317
319
|
}
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
*/
|
|
325
|
-
if (!dsl || wrapperType === 'regular') {
|
|
322
|
+
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
323
|
+
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
324
|
+
// with identifier args, etc.) are still registered as invoked functions.
|
|
325
|
+
if (wrapperType === 'complex') {
|
|
326
326
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
327
327
|
}
|
|
328
328
|
|
|
@@ -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
|
|
|
4
5
|
export type ExtractedFunctionName = {
|
|
5
6
|
pikkuFuncId: string
|
|
@@ -8,6 +9,7 @@ export type ExtractedFunctionName = {
|
|
|
8
9
|
exportedName: string | null
|
|
9
10
|
propertyName: string | null
|
|
10
11
|
isHelper: boolean
|
|
12
|
+
version: number | null
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function makeContextBasedId(
|
|
@@ -40,6 +42,7 @@ export function extractFunctionName(
|
|
|
40
42
|
propertyName: null,
|
|
41
43
|
explicitName: null,
|
|
42
44
|
isHelper: false,
|
|
45
|
+
version: null,
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const workflowHelpers = new Set([
|
|
@@ -143,18 +146,7 @@ export function extractFunctionName(
|
|
|
143
146
|
// Check for object with 'name' property in first argument
|
|
144
147
|
const firstArg = args[0]
|
|
145
148
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
ts.isPropertyAssignment(prop) &&
|
|
149
|
-
ts.isIdentifier(prop.name) &&
|
|
150
|
-
prop.name.text === 'override' &&
|
|
151
|
-
ts.isStringLiteral(prop.initializer)
|
|
152
|
-
) {
|
|
153
|
-
// Priority 1: Object with override property
|
|
154
|
-
result.explicitName = prop.initializer.text
|
|
155
|
-
break
|
|
156
|
-
}
|
|
157
|
-
}
|
|
149
|
+
extractOverrideAndVersion(firstArg, result)
|
|
158
150
|
}
|
|
159
151
|
|
|
160
152
|
// Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
|
|
@@ -367,21 +359,9 @@ export function extractFunctionName(
|
|
|
367
359
|
ts.isIdentifier(decl.initializer.expression) &&
|
|
368
360
|
decl.initializer.expression.text.startsWith('pikku')
|
|
369
361
|
) {
|
|
370
|
-
// Check for object with 'override' property in first argument
|
|
371
362
|
const firstArg = decl.initializer.arguments[0]
|
|
372
363
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
ts.isPropertyAssignment(prop) &&
|
|
376
|
-
ts.isIdentifier(prop.name) &&
|
|
377
|
-
prop.name.text === 'override' &&
|
|
378
|
-
ts.isStringLiteral(prop.initializer)
|
|
379
|
-
) {
|
|
380
|
-
// Priority 1: Object with override property
|
|
381
|
-
result.explicitName = prop.initializer.text
|
|
382
|
-
break
|
|
383
|
-
}
|
|
384
|
-
}
|
|
364
|
+
extractOverrideAndVersion(firstArg, result)
|
|
385
365
|
}
|
|
386
366
|
|
|
387
367
|
if (decl.initializer.expression.text.startsWith('pikku')) {
|
|
@@ -448,18 +428,7 @@ export function extractFunctionName(
|
|
|
448
428
|
else if (ts.isCallExpression(callExpr)) {
|
|
449
429
|
const firstArg = callExpr.arguments[0]
|
|
450
430
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
ts.isPropertyAssignment(prop) &&
|
|
454
|
-
ts.isIdentifier(prop.name) &&
|
|
455
|
-
prop.name.text === 'override' &&
|
|
456
|
-
ts.isStringLiteral(prop.initializer) &&
|
|
457
|
-
!result.explicitName // Only set if not already set
|
|
458
|
-
) {
|
|
459
|
-
result.explicitName = prop.initializer.text
|
|
460
|
-
break
|
|
461
|
-
}
|
|
462
|
-
}
|
|
431
|
+
extractOverrideAndVersion(firstArg, result)
|
|
463
432
|
}
|
|
464
433
|
}
|
|
465
434
|
|
|
@@ -474,6 +443,10 @@ export function extractFunctionName(
|
|
|
474
443
|
result.pikkuFuncId = `__temp_${randomUUID()}`
|
|
475
444
|
}
|
|
476
445
|
|
|
446
|
+
if (result.version !== null) {
|
|
447
|
+
result.pikkuFuncId = formatVersionedId(result.pikkuFuncId, result.version)
|
|
448
|
+
}
|
|
449
|
+
|
|
477
450
|
return result
|
|
478
451
|
}
|
|
479
452
|
|
|
@@ -539,3 +512,29 @@ export function isNamedExport(
|
|
|
539
512
|
|
|
540
513
|
return false
|
|
541
514
|
}
|
|
515
|
+
|
|
516
|
+
function extractOverrideAndVersion(
|
|
517
|
+
objLiteral: ts.ObjectLiteralExpression,
|
|
518
|
+
result: ExtractedFunctionName
|
|
519
|
+
): void {
|
|
520
|
+
for (const prop of objLiteral.properties) {
|
|
521
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
522
|
+
if (
|
|
523
|
+
prop.name.text === 'override' &&
|
|
524
|
+
ts.isStringLiteral(prop.initializer) &&
|
|
525
|
+
!result.explicitName
|
|
526
|
+
) {
|
|
527
|
+
result.explicitName = prop.initializer.text
|
|
528
|
+
} else if (
|
|
529
|
+
prop.name.text === 'version' &&
|
|
530
|
+
ts.isNumericLiteral(prop.initializer) &&
|
|
531
|
+
result.version === null
|
|
532
|
+
) {
|
|
533
|
+
const parsed = Number(prop.initializer.text)
|
|
534
|
+
if (Number.isInteger(parsed) && parsed >= 1) {
|
|
535
|
+
result.version = parsed
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import * as ts from 'typescript'
|
|
4
|
+
import { extractDescription } from './extract-node-value'
|
|
5
|
+
|
|
6
|
+
const createChecker = (source: string) => {
|
|
7
|
+
const sourceFile = ts.createSourceFile(
|
|
8
|
+
'test.ts',
|
|
9
|
+
source,
|
|
10
|
+
ts.ScriptTarget.Latest,
|
|
11
|
+
true,
|
|
12
|
+
ts.ScriptKind.TS
|
|
13
|
+
)
|
|
14
|
+
const host = ts.createCompilerHost({})
|
|
15
|
+
const originalGetSourceFile = host.getSourceFile
|
|
16
|
+
host.getSourceFile = (fileName, target) => {
|
|
17
|
+
if (fileName === 'test.ts') return sourceFile
|
|
18
|
+
return originalGetSourceFile.call(host, fileName, target)
|
|
19
|
+
}
|
|
20
|
+
const program = ts.createProgram(['test.ts'], {}, host)
|
|
21
|
+
return { checker: program.getTypeChecker(), sourceFile }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const findObjectLiteral = (
|
|
25
|
+
node: ts.Node
|
|
26
|
+
): ts.ObjectLiteralExpression | undefined => {
|
|
27
|
+
if (ts.isObjectLiteralExpression(node)) return node
|
|
28
|
+
let result: ts.ObjectLiteralExpression | undefined
|
|
29
|
+
ts.forEachChild(node, (child) => {
|
|
30
|
+
if (!result) result = findObjectLiteral(child)
|
|
31
|
+
})
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('extractDescription', () => {
|
|
36
|
+
test('returns null when node is undefined', () => {
|
|
37
|
+
const { checker } = createChecker('')
|
|
38
|
+
assert.equal(extractDescription(undefined, checker), null)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('extracts string literal description', () => {
|
|
42
|
+
const { checker, sourceFile } = createChecker(
|
|
43
|
+
`const opts = { description: 'my step' }`
|
|
44
|
+
)
|
|
45
|
+
const obj = findObjectLiteral(sourceFile)!
|
|
46
|
+
assert.equal(extractDescription(obj, checker), 'my step')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('returns null for non-literal description value without crashing', () => {
|
|
50
|
+
const { checker, sourceFile } = createChecker(
|
|
51
|
+
`const name = 'test'; const data = { description: name + ' addon' }`
|
|
52
|
+
)
|
|
53
|
+
const objs: ts.ObjectLiteralExpression[] = []
|
|
54
|
+
const visit = (node: ts.Node) => {
|
|
55
|
+
if (ts.isObjectLiteralExpression(node)) objs.push(node)
|
|
56
|
+
ts.forEachChild(node, visit)
|
|
57
|
+
}
|
|
58
|
+
ts.forEachChild(sourceFile, visit)
|
|
59
|
+
const dataObj = objs[objs.length - 1]!
|
|
60
|
+
assert.equal(extractDescription(dataObj, checker), null)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('returns null for non-object node', () => {
|
|
64
|
+
const { checker, sourceFile } = createChecker(`const x = 42`)
|
|
65
|
+
assert.equal(extractDescription(sourceFile, checker), null)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -27,6 +27,11 @@ export function extractStringLiteral(
|
|
|
27
27
|
return result
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
31
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
32
|
+
return extractStringLiteral(node.expression, checker)
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
// Try to evaluate constant identifiers
|
|
31
36
|
if (ts.isIdentifier(node)) {
|
|
32
37
|
const symbol = checker.getSymbolAtLocation(node)
|
|
@@ -110,7 +115,11 @@ export function extractDescription(
|
|
|
110
115
|
if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
|
|
111
116
|
return null
|
|
112
117
|
}
|
|
113
|
-
|
|
118
|
+
try {
|
|
119
|
+
return extractPropertyString(optionsNode, 'description', checker)
|
|
120
|
+
} catch {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
/**
|
|
@@ -730,11 +730,15 @@ export function filterInspectorState(
|
|
|
730
730
|
}
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
-
// Post-filter version expansion:
|
|
733
|
+
// Post-filter version expansion: when an unversioned base name is matched,
|
|
734
|
+
// include all its versions. Specific version matches (e.g. analyzeData@v1)
|
|
735
|
+
// do NOT expand to include other versions.
|
|
734
736
|
const includedBaseNames = new Set<string>()
|
|
735
737
|
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
736
|
-
const { baseName } = parseVersionedId(funcId)
|
|
737
|
-
|
|
738
|
+
const { baseName, version } = parseVersionedId(funcId)
|
|
739
|
+
if (version === null) {
|
|
740
|
+
includedBaseNames.add(baseName)
|
|
741
|
+
}
|
|
738
742
|
}
|
|
739
743
|
if (includedBaseNames.size > 0) {
|
|
740
744
|
for (const funcId of Object.keys(state.functions.meta)) {
|
|
@@ -215,6 +215,147 @@ describe('resolveLatestVersions', () => {
|
|
|
215
215
|
assert.strictEqual(errors.length, 0)
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
+
test('exposedMeta propagates all versions when base name is exposed', () => {
|
|
219
|
+
const state = makeState({
|
|
220
|
+
'createUser@v1': {
|
|
221
|
+
pikkuFuncId: 'createUser@v1',
|
|
222
|
+
inputSchemaName: null,
|
|
223
|
+
outputSchemaName: null,
|
|
224
|
+
version: 1,
|
|
225
|
+
},
|
|
226
|
+
createUser: {
|
|
227
|
+
pikkuFuncId: 'createUser',
|
|
228
|
+
inputSchemaName: null,
|
|
229
|
+
outputSchemaName: null,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
state.rpc.exposedMeta['createUser'] = 'createUser'
|
|
233
|
+
state.rpc.internalFiles.set('createUser@v1', {
|
|
234
|
+
path: '/src/user.ts',
|
|
235
|
+
exportedName: 'createUserV1',
|
|
236
|
+
})
|
|
237
|
+
const { logger } = makeLogger()
|
|
238
|
+
|
|
239
|
+
resolveLatestVersions(state, logger)
|
|
240
|
+
|
|
241
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser'], 'createUser@v2')
|
|
242
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v1'], 'createUser@v1')
|
|
243
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v2'], 'createUser@v2')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('exposedFiles propagated for versioned exposed functions', () => {
|
|
247
|
+
const state = makeState({
|
|
248
|
+
'createUser@v1': {
|
|
249
|
+
pikkuFuncId: 'createUser@v1',
|
|
250
|
+
inputSchemaName: null,
|
|
251
|
+
outputSchemaName: null,
|
|
252
|
+
version: 1,
|
|
253
|
+
},
|
|
254
|
+
createUser: {
|
|
255
|
+
pikkuFuncId: 'createUser',
|
|
256
|
+
inputSchemaName: null,
|
|
257
|
+
outputSchemaName: null,
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
state.rpc.exposedMeta['createUser'] = 'createUser'
|
|
261
|
+
state.rpc.internalFiles.set('createUser@v1', {
|
|
262
|
+
path: '/src/user-v1.ts',
|
|
263
|
+
exportedName: 'createUserV1',
|
|
264
|
+
})
|
|
265
|
+
state.rpc.internalFiles.set('createUser', {
|
|
266
|
+
path: '/src/user.ts',
|
|
267
|
+
exportedName: 'createUser',
|
|
268
|
+
})
|
|
269
|
+
const { logger } = makeLogger()
|
|
270
|
+
|
|
271
|
+
resolveLatestVersions(state, logger)
|
|
272
|
+
|
|
273
|
+
assert.ok(state.rpc.exposedFiles.has('createUser@v1'))
|
|
274
|
+
assert.strictEqual(
|
|
275
|
+
state.rpc.exposedFiles.get('createUser@v1')!.exportedName,
|
|
276
|
+
'createUserV1'
|
|
277
|
+
)
|
|
278
|
+
assert.ok(state.rpc.exposedFiles.has('createUser@v2'))
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('exposedMeta unchanged when base name is not exposed', () => {
|
|
282
|
+
const state = makeState({
|
|
283
|
+
'createUser@v1': {
|
|
284
|
+
pikkuFuncId: 'createUser@v1',
|
|
285
|
+
inputSchemaName: null,
|
|
286
|
+
outputSchemaName: null,
|
|
287
|
+
version: 1,
|
|
288
|
+
},
|
|
289
|
+
createUser: {
|
|
290
|
+
pikkuFuncId: 'createUser',
|
|
291
|
+
inputSchemaName: null,
|
|
292
|
+
outputSchemaName: null,
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
const { logger } = makeLogger()
|
|
296
|
+
|
|
297
|
+
resolveLatestVersions(state, logger)
|
|
298
|
+
|
|
299
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v1'], undefined)
|
|
300
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v2'], undefined)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('exposedMeta propagates when only explicit versions exist', () => {
|
|
304
|
+
const state = makeState({
|
|
305
|
+
'createUser@v1': {
|
|
306
|
+
pikkuFuncId: 'createUser@v1',
|
|
307
|
+
inputSchemaName: null,
|
|
308
|
+
outputSchemaName: null,
|
|
309
|
+
version: 1,
|
|
310
|
+
},
|
|
311
|
+
'createUser@v2': {
|
|
312
|
+
pikkuFuncId: 'createUser@v2',
|
|
313
|
+
inputSchemaName: null,
|
|
314
|
+
outputSchemaName: null,
|
|
315
|
+
version: 2,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
state.rpc.exposedMeta['createUser'] = 'createUser@v1'
|
|
319
|
+
const { logger } = makeLogger()
|
|
320
|
+
|
|
321
|
+
resolveLatestVersions(state, logger)
|
|
322
|
+
|
|
323
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser'], 'createUser@v2')
|
|
324
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v1'], 'createUser@v1')
|
|
325
|
+
assert.strictEqual(state.rpc.exposedMeta['createUser@v2'], 'createUser@v2')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('updates HTTP meta pikkuFuncId when renaming unversioned', () => {
|
|
329
|
+
const state = makeState({
|
|
330
|
+
'createUser@v1': {
|
|
331
|
+
pikkuFuncId: 'createUser@v1',
|
|
332
|
+
inputSchemaName: null,
|
|
333
|
+
outputSchemaName: null,
|
|
334
|
+
version: 1,
|
|
335
|
+
},
|
|
336
|
+
createUser: {
|
|
337
|
+
pikkuFuncId: 'createUser',
|
|
338
|
+
inputSchemaName: null,
|
|
339
|
+
outputSchemaName: null,
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
;(state as any).http = {
|
|
343
|
+
meta: {
|
|
344
|
+
post: {
|
|
345
|
+
'/api/users': { pikkuFuncId: 'createUser', route: '/api/users' },
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
const { logger } = makeLogger()
|
|
350
|
+
|
|
351
|
+
resolveLatestVersions(state, logger)
|
|
352
|
+
|
|
353
|
+
assert.strictEqual(
|
|
354
|
+
(state as any).http.meta.post['/api/users'].pikkuFuncId,
|
|
355
|
+
'createUser@v2'
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
218
359
|
test('renames files entries when renaming unversioned to versioned', () => {
|
|
219
360
|
const state = makeState({
|
|
220
361
|
'createUser@v1': {
|
|
@@ -91,6 +91,8 @@ export function resolveLatestVersions(
|
|
|
91
91
|
if (state.rpc.exposedMeta[baseName] === oldId) {
|
|
92
92
|
state.rpc.exposedMeta[baseName] = newId
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
updateWiringReferences(state, oldId, newId)
|
|
94
96
|
} else {
|
|
95
97
|
const latest = group.explicit.reduce((a, b) =>
|
|
96
98
|
a.version > b.version ? a : b
|
|
@@ -98,8 +100,43 @@ export function resolveLatestVersions(
|
|
|
98
100
|
state.rpc.internalMeta[baseName] = latest.id
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
if (state.rpc.exposedMeta[baseName]) {
|
|
104
|
+
const latestId = state.rpc.internalMeta[baseName]!
|
|
105
|
+
state.rpc.exposedMeta[baseName] = latestId
|
|
106
|
+
for (const entry of group.explicit) {
|
|
107
|
+
state.rpc.exposedMeta[entry.id] = entry.id
|
|
108
|
+
const fileEntry = state.rpc.internalFiles.get(entry.id)
|
|
109
|
+
if (fileEntry) {
|
|
110
|
+
state.rpc.exposedFiles.set(entry.id, fileEntry)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (group.unversioned) {
|
|
114
|
+
state.rpc.exposedMeta[latestId] = latestId
|
|
115
|
+
const fileEntry = state.rpc.internalFiles.get(latestId)
|
|
116
|
+
if (fileEntry) {
|
|
117
|
+
state.rpc.exposedFiles.set(latestId, fileEntry)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
for (const entry of group.explicit) {
|
|
102
123
|
state.rpc.invokedFunctions.add(entry.id)
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
}
|
|
127
|
+
|
|
128
|
+
function updateWiringReferences(
|
|
129
|
+
state: InspectorState,
|
|
130
|
+
oldId: string,
|
|
131
|
+
newId: string
|
|
132
|
+
): void {
|
|
133
|
+
if (state.http) {
|
|
134
|
+
for (const methods of Object.values(state.http.meta)) {
|
|
135
|
+
for (const meta of Object.values(methods)) {
|
|
136
|
+
if (meta.pikkuFuncId === oldId) {
|
|
137
|
+
meta.pikkuFuncId = newId
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|