@pikku/inspector 0.10.1 → 0.11.0
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 +54 -0
- package/dist/add/add-channel.js +68 -14
- package/dist/add/add-functions.js +9 -2
- package/dist/add/add-workflow.d.ts +6 -0
- package/dist/add/add-workflow.js +152 -0
- package/dist/error-codes.d.ts +4 -1
- package/dist/error-codes.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inspector.d.ts +6 -0
- package/dist/inspector.js +53 -15
- package/dist/types.d.ts +10 -2
- package/dist/utils/extract-node-value.d.ts +24 -0
- package/dist/utils/extract-node-value.js +79 -0
- package/dist/utils/post-process.d.ts +1 -1
- package/dist/utils/post-process.js +30 -0
- package/dist/utils/serialize-inspector-state.d.ts +6 -0
- package/dist/utils/serialize-inspector-state.js +12 -0
- package/dist/utils/type-utils.d.ts +4 -0
- package/dist/utils/type-utils.js +60 -3
- package/dist/visit.js +2 -0
- package/package.json +2 -2
- package/src/add/add-channel.ts +94 -19
- package/src/add/add-functions.ts +10 -2
- package/src/add/add-workflow.ts +231 -0
- package/src/error-codes.ts +5 -0
- package/src/index.ts +1 -1
- package/src/inspector.ts +77 -22
- package/src/types.ts +10 -2
- package/src/utils/extract-node-value.ts +101 -0
- package/src/utils/post-process.ts +40 -2
- package/src/utils/serialize-inspector-state.ts +18 -0
- package/src/utils/test-data/inspector-state.json +4 -0
- package/src/utils/type-utils.ts +74 -3
- package/src/visit.ts +3 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/add/add-mcp-prompt.ts.tmp +0 -0
- package/src/add/add-mcp-resource.ts.tmp +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import {
|
|
3
|
+
getPropertyValue,
|
|
4
|
+
getPropertyTags,
|
|
5
|
+
} from '../utils/get-property-value.js'
|
|
6
|
+
import { PikkuDocs } from '@pikku/core'
|
|
7
|
+
import { AddWiring, InspectorState } from '../types.js'
|
|
8
|
+
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
9
|
+
import {
|
|
10
|
+
getPropertyAssignmentInitializer,
|
|
11
|
+
resolveFunctionDeclaration,
|
|
12
|
+
} from '../utils/type-utils.js'
|
|
13
|
+
import { resolveMiddleware } from '../utils/middleware.js'
|
|
14
|
+
import { extractWireNames } from '../utils/post-process.js'
|
|
15
|
+
import { ErrorCode } from '../error-codes.js'
|
|
16
|
+
import { WorkflowStepMeta } from '@pikku/core/workflow'
|
|
17
|
+
import {
|
|
18
|
+
extractStringLiteral,
|
|
19
|
+
extractNumberLiteral,
|
|
20
|
+
extractPropertyString,
|
|
21
|
+
isStringLike,
|
|
22
|
+
isFunctionLike,
|
|
23
|
+
} from '../utils/extract-node-value.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scan for workflow.do() and workflow.sleep() calls to extract workflow steps
|
|
27
|
+
*/
|
|
28
|
+
function getWorkflowInvocations(
|
|
29
|
+
node: ts.Node,
|
|
30
|
+
checker: ts.TypeChecker,
|
|
31
|
+
state: InspectorState,
|
|
32
|
+
workflowName: string,
|
|
33
|
+
steps: WorkflowStepMeta[]
|
|
34
|
+
) {
|
|
35
|
+
// Look for property access expressions: workflow.do or workflow.sleep
|
|
36
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
37
|
+
const { name } = node
|
|
38
|
+
|
|
39
|
+
// Check if this is accessing 'do' or 'sleep' property
|
|
40
|
+
if (name.text === 'do' || name.text === 'sleep') {
|
|
41
|
+
// Check if the parent is a call expression
|
|
42
|
+
const parent = node.parent
|
|
43
|
+
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
44
|
+
const args = parent.arguments
|
|
45
|
+
|
|
46
|
+
if (name.text === 'do' && args.length >= 2) {
|
|
47
|
+
// workflow.do(stepName, rpcName|fn, data?, options?)
|
|
48
|
+
const stepNameArg = args[0]
|
|
49
|
+
const secondArg = args[1]
|
|
50
|
+
const optionsArg =
|
|
51
|
+
args.length >= 3 ? args[args.length - 1] : undefined
|
|
52
|
+
|
|
53
|
+
const stepName = extractStringLiteral(stepNameArg, checker)
|
|
54
|
+
const description =
|
|
55
|
+
extractDescription(optionsArg, checker) ?? undefined
|
|
56
|
+
|
|
57
|
+
// Determine form by checking 2nd argument type
|
|
58
|
+
if (isStringLike(secondArg, checker)) {
|
|
59
|
+
// RPC form: workflow.do(stepName, rpcName, data, options?)
|
|
60
|
+
const rpcName = extractStringLiteral(secondArg, checker)
|
|
61
|
+
steps.push({
|
|
62
|
+
type: 'rpc',
|
|
63
|
+
stepName,
|
|
64
|
+
rpcName,
|
|
65
|
+
description,
|
|
66
|
+
})
|
|
67
|
+
state.rpc.invokedFunctions.add(rpcName)
|
|
68
|
+
} else if (isFunctionLike(secondArg)) {
|
|
69
|
+
// Inline form: workflow.do(stepName, fn, options?)
|
|
70
|
+
steps.push({
|
|
71
|
+
type: 'inline',
|
|
72
|
+
stepName: stepName || '<dynamic>',
|
|
73
|
+
description: description || '<dynamic>',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
} else if (name.text === 'sleep' && args.length >= 2) {
|
|
77
|
+
// workflow.sleep(stepName, duration)
|
|
78
|
+
const stepNameArg = args[0]
|
|
79
|
+
const durationArg = args[1]
|
|
80
|
+
|
|
81
|
+
const stepName = extractStringLiteral(stepNameArg, checker)
|
|
82
|
+
const duration = extractDuration(durationArg, checker)
|
|
83
|
+
|
|
84
|
+
steps.push({
|
|
85
|
+
type: 'sleep',
|
|
86
|
+
stepName: stepName || '<dynamic>',
|
|
87
|
+
duration: duration || '<dynamic>',
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Don't recurse into nested functions - only look at top-level workflow calls
|
|
95
|
+
ts.forEachChild(node, (child) => {
|
|
96
|
+
if (
|
|
97
|
+
ts.isFunctionDeclaration(child) ||
|
|
98
|
+
ts.isFunctionExpression(child) ||
|
|
99
|
+
ts.isArrowFunction(child)
|
|
100
|
+
) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
getWorkflowInvocations(child, checker, state, workflowName, steps)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract description from options object
|
|
109
|
+
*/
|
|
110
|
+
function extractDescription(
|
|
111
|
+
optionsNode: ts.Node | undefined,
|
|
112
|
+
checker: ts.TypeChecker
|
|
113
|
+
): string | null {
|
|
114
|
+
if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
return extractPropertyString(optionsNode, 'description', checker)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract duration value (number or string)
|
|
122
|
+
*/
|
|
123
|
+
function extractDuration(
|
|
124
|
+
node: ts.Node,
|
|
125
|
+
checker: ts.TypeChecker
|
|
126
|
+
): string | number | null {
|
|
127
|
+
const numValue = extractNumberLiteral(node)
|
|
128
|
+
if (numValue !== null) {
|
|
129
|
+
return numValue
|
|
130
|
+
}
|
|
131
|
+
return extractStringLiteral(node, checker)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Inspector for wireWorkflow() calls
|
|
136
|
+
* Detects workflow registration and extracts metadata
|
|
137
|
+
*/
|
|
138
|
+
export const addWorkflow: AddWiring = (
|
|
139
|
+
logger,
|
|
140
|
+
node,
|
|
141
|
+
checker,
|
|
142
|
+
state,
|
|
143
|
+
options
|
|
144
|
+
) => {
|
|
145
|
+
if (!ts.isCallExpression(node)) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const args = node.arguments
|
|
150
|
+
const firstArg = args[0]
|
|
151
|
+
const expression = node.expression
|
|
152
|
+
|
|
153
|
+
// Check if the call is to wireWorkflow
|
|
154
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!firstArg) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
163
|
+
const obj = firstArg
|
|
164
|
+
|
|
165
|
+
const workflowName = getPropertyValue(obj, 'name') as string | null
|
|
166
|
+
const description = getPropertyValue(obj, 'description') as
|
|
167
|
+
| string
|
|
168
|
+
| undefined
|
|
169
|
+
const docs = (getPropertyValue(obj, 'docs') as PikkuDocs) || undefined
|
|
170
|
+
const tags = getPropertyTags(obj, 'Workflow', workflowName, logger)
|
|
171
|
+
|
|
172
|
+
// --- find the referenced function ---
|
|
173
|
+
const funcInitializer = getPropertyAssignmentInitializer(
|
|
174
|
+
obj,
|
|
175
|
+
'func',
|
|
176
|
+
true,
|
|
177
|
+
checker
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (!workflowName) {
|
|
181
|
+
logger.critical(
|
|
182
|
+
ErrorCode.MISSING_NAME,
|
|
183
|
+
`Wasn't able to determine 'name' property for workflow wiring.`
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!funcInitializer) {
|
|
189
|
+
logger.critical(
|
|
190
|
+
ErrorCode.MISSING_FUNC,
|
|
191
|
+
`No valid 'func' property for workflow '${workflowName}'.`
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pikkuFuncName = extractFunctionName(
|
|
197
|
+
funcInitializer,
|
|
198
|
+
checker,
|
|
199
|
+
state.rootDir
|
|
200
|
+
).pikkuFuncName
|
|
201
|
+
|
|
202
|
+
// --- resolve middleware ---
|
|
203
|
+
const middleware = resolveMiddleware(state, obj, tags, checker)
|
|
204
|
+
|
|
205
|
+
// --- track used functions/middleware for service aggregation ---
|
|
206
|
+
state.serviceAggregation.usedFunctions.add(pikkuFuncName)
|
|
207
|
+
extractWireNames(middleware).forEach((name) =>
|
|
208
|
+
state.serviceAggregation.usedMiddleware.add(name)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
state.workflows.files.add(node.getSourceFile().fileName)
|
|
212
|
+
|
|
213
|
+
// Extract workflow steps from function body
|
|
214
|
+
// Resolve the identifier to the actual function declaration
|
|
215
|
+
const resolvedFunc = resolveFunctionDeclaration(funcInitializer, checker)
|
|
216
|
+
const steps: WorkflowStepMeta[] = []
|
|
217
|
+
if (resolvedFunc) {
|
|
218
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
state.workflows.meta[workflowName] = {
|
|
222
|
+
pikkuFuncName,
|
|
223
|
+
workflowName,
|
|
224
|
+
description,
|
|
225
|
+
docs,
|
|
226
|
+
tags,
|
|
227
|
+
middleware,
|
|
228
|
+
steps,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/error-codes.ts
CHANGED
|
@@ -19,6 +19,8 @@ export enum ErrorCode {
|
|
|
19
19
|
MISSING_QUEUE_NAME = 'PKU384',
|
|
20
20
|
MISSING_CHANNEL_NAME = 'PKU400',
|
|
21
21
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = 'PKU672',
|
|
22
|
+
DYNAMIC_STEP_NAME = 'PKU529',
|
|
23
|
+
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
|
|
22
24
|
|
|
23
25
|
// Configuration errors
|
|
24
26
|
CONFIG_TYPE_NOT_FOUND = 'PKU426',
|
|
@@ -40,4 +42,7 @@ export enum ErrorCode {
|
|
|
40
42
|
PERMISSION_TAG_INVALID = 'PKU836',
|
|
41
43
|
PERMISSION_EMPTY_ARRAY = 'PKU937',
|
|
42
44
|
PERMISSION_PATTERN_INVALID = 'PKU975',
|
|
45
|
+
|
|
46
|
+
// Feature Flag
|
|
47
|
+
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = 'PKU901',
|
|
43
48
|
}
|
package/src/index.ts
CHANGED
package/src/inspector.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import { performance } from 'perf_hooks'
|
|
2
3
|
import { visitSetup, visitRoutes } from './visit.js'
|
|
3
4
|
import { TypesMap } from './types-map.js'
|
|
4
5
|
import { InspectorState, InspectorLogger, InspectorOptions } from './types.js'
|
|
@@ -6,22 +7,13 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js'
|
|
|
6
7
|
import { findCommonAncestor } from './utils/find-root-dir.js'
|
|
7
8
|
import { aggregateRequiredServices } from './utils/post-process.js'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
module: ts.ModuleKind.CommonJS,
|
|
17
|
-
})
|
|
18
|
-
const checker = program.getTypeChecker()
|
|
19
|
-
const sourceFiles = program.getSourceFiles()
|
|
20
|
-
|
|
21
|
-
// Infer root directory from source files
|
|
22
|
-
const rootDir = findCommonAncestor(routeFiles)
|
|
23
|
-
|
|
24
|
-
const state: InspectorState = {
|
|
10
|
+
/**
|
|
11
|
+
* Creates an initial/empty inspector state with all required properties initialized
|
|
12
|
+
* @param rootDir - The root directory for the project
|
|
13
|
+
* @returns A fresh InspectorState with empty collections
|
|
14
|
+
*/
|
|
15
|
+
export function getInitialInspectorState(rootDir: string): InspectorState {
|
|
16
|
+
return {
|
|
25
17
|
rootDir,
|
|
26
18
|
singletonServicesTypeImportMap: new Map(),
|
|
27
19
|
sessionServicesTypeImportMap: new Map(),
|
|
@@ -66,6 +58,10 @@ export const inspect = (
|
|
|
66
58
|
meta: {},
|
|
67
59
|
files: new Set(),
|
|
68
60
|
},
|
|
61
|
+
workflows: {
|
|
62
|
+
meta: {},
|
|
63
|
+
files: new Set(),
|
|
64
|
+
},
|
|
69
65
|
rpc: {
|
|
70
66
|
internalMeta: {},
|
|
71
67
|
internalFiles: new Map(),
|
|
@@ -99,30 +95,89 @@ export const inspect = (
|
|
|
99
95
|
usedFunctions: new Set(),
|
|
100
96
|
usedMiddleware: new Set(),
|
|
101
97
|
usedPermissions: new Set(),
|
|
98
|
+
allSingletonServices: [],
|
|
99
|
+
allSessionServices: [],
|
|
102
100
|
},
|
|
103
101
|
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const inspect = (
|
|
105
|
+
logger: InspectorLogger,
|
|
106
|
+
routeFiles: string[],
|
|
107
|
+
options: InspectorOptions = {}
|
|
108
|
+
): InspectorState => {
|
|
109
|
+
const startProgram = performance.now()
|
|
110
|
+
const program = ts.createProgram(routeFiles, {
|
|
111
|
+
target: ts.ScriptTarget.ESNext,
|
|
112
|
+
module: ts.ModuleKind.CommonJS,
|
|
113
|
+
skipLibCheck: true,
|
|
114
|
+
skipDefaultLibCheck: true,
|
|
115
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
116
|
+
types: [],
|
|
117
|
+
allowJs: false,
|
|
118
|
+
checkJs: false,
|
|
119
|
+
})
|
|
120
|
+
logger.debug(
|
|
121
|
+
`Created program in ${(performance.now() - startProgram).toFixed(2)}ms`
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const startChecker = performance.now()
|
|
125
|
+
const checker = program.getTypeChecker()
|
|
126
|
+
logger.debug(
|
|
127
|
+
`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const startSourceFiles = performance.now()
|
|
131
|
+
const sourceFiles = program.getSourceFiles()
|
|
132
|
+
logger.debug(
|
|
133
|
+
`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Infer root directory from source files
|
|
137
|
+
const rootDir = findCommonAncestor(routeFiles)
|
|
138
|
+
|
|
139
|
+
const state = getInitialInspectorState(rootDir)
|
|
104
140
|
|
|
105
141
|
// First sweep: add all functions
|
|
142
|
+
const startSetup = performance.now()
|
|
106
143
|
for (const sourceFile of sourceFiles) {
|
|
107
144
|
ts.forEachChild(sourceFile, (child) =>
|
|
108
145
|
visitSetup(logger, checker, child, state, options)
|
|
109
146
|
)
|
|
110
147
|
}
|
|
148
|
+
logger.debug(
|
|
149
|
+
`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
|
|
150
|
+
)
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
if (!options.setupOnly) {
|
|
153
|
+
// Second sweep: add all transports
|
|
154
|
+
const startRoutes = performance.now()
|
|
155
|
+
for (const sourceFile of sourceFiles) {
|
|
156
|
+
ts.forEachChild(sourceFile, (child) =>
|
|
157
|
+
visitRoutes(logger, checker, child, state, options)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
logger.debug(
|
|
161
|
+
`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`
|
|
116
162
|
)
|
|
117
163
|
}
|
|
118
164
|
|
|
119
165
|
// Populate filesAndMethods
|
|
166
|
+
const startFilesAndMethods = performance.now()
|
|
120
167
|
const { result, errors } = getFilesAndMethods(state, options.types)
|
|
121
168
|
state.filesAndMethods = result
|
|
122
169
|
state.filesAndMethodsErrors = errors
|
|
170
|
+
logger.debug(
|
|
171
|
+
`Get files and methods completed in ${(performance.now() - startFilesAndMethods).toFixed(2)}ms`
|
|
172
|
+
)
|
|
123
173
|
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
if (!options.setupOnly) {
|
|
175
|
+
const startAggregate = performance.now()
|
|
176
|
+
aggregateRequiredServices(state)
|
|
177
|
+
logger.debug(
|
|
178
|
+
`Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
126
181
|
|
|
127
182
|
return state
|
|
128
183
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,8 @@ import * as ts from 'typescript'
|
|
|
2
2
|
import { ChannelsMeta } from '@pikku/core/channel'
|
|
3
3
|
import { HTTPWiringsMeta } from '@pikku/core/http'
|
|
4
4
|
import { ScheduledTasksMeta } from '@pikku/core/scheduler'
|
|
5
|
-
import {
|
|
5
|
+
import { QueueWorkersMeta } from '@pikku/core/queue'
|
|
6
|
+
import { WorkflowsMeta } from '@pikku/core/workflow'
|
|
6
7
|
import { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp'
|
|
7
8
|
import { CLIMeta } from '@pikku/core/cli'
|
|
8
9
|
import { TypesMap } from './types-map.js'
|
|
@@ -114,6 +115,7 @@ export type InspectorFilters = {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export type InspectorOptions = Partial<{
|
|
118
|
+
setupOnly: boolean
|
|
117
119
|
types: Partial<{
|
|
118
120
|
configFileType: string
|
|
119
121
|
userSessionType: string
|
|
@@ -204,7 +206,11 @@ export interface InspectorState {
|
|
|
204
206
|
files: Set<string>
|
|
205
207
|
}
|
|
206
208
|
queueWorkers: {
|
|
207
|
-
meta:
|
|
209
|
+
meta: QueueWorkersMeta
|
|
210
|
+
files: Set<string>
|
|
211
|
+
}
|
|
212
|
+
workflows: {
|
|
213
|
+
meta: WorkflowsMeta
|
|
208
214
|
files: Set<string>
|
|
209
215
|
}
|
|
210
216
|
rpc: {
|
|
@@ -231,5 +237,7 @@ export interface InspectorState {
|
|
|
231
237
|
usedFunctions: Set<string> // Function names actually wired/exposed
|
|
232
238
|
usedMiddleware: Set<string> // Middleware names used by wired functions
|
|
233
239
|
usedPermissions: Set<string> // Permission names used by wired functions
|
|
240
|
+
allSingletonServices: string[] // All services available in SingletonServices type
|
|
241
|
+
allSessionServices: string[] // All services available in Services type (excluding SingletonServices)
|
|
234
242
|
}
|
|
235
243
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract string literal value from a TypeScript node.
|
|
5
|
+
* Handles string literals, template literals (including placeholders),
|
|
6
|
+
* and constant variable references.
|
|
7
|
+
*/
|
|
8
|
+
export function extractStringLiteral(
|
|
9
|
+
node: ts.Node,
|
|
10
|
+
checker: ts.TypeChecker
|
|
11
|
+
): string {
|
|
12
|
+
if (ts.isStringLiteral(node)) {
|
|
13
|
+
return node.text
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
17
|
+
return node.text
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (ts.isTemplateExpression(node)) {
|
|
21
|
+
// reconstruct: `head + ${expr} + middle + ${expr} + tail`
|
|
22
|
+
let result = node.head.text
|
|
23
|
+
for (const span of node.templateSpans) {
|
|
24
|
+
const exprText = span.expression.getText()
|
|
25
|
+
result += '${' + exprText + '}' + span.literal.text
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Try to evaluate constant identifiers
|
|
31
|
+
if (ts.isIdentifier(node)) {
|
|
32
|
+
const symbol = checker.getSymbolAtLocation(node)
|
|
33
|
+
if (
|
|
34
|
+
symbol?.valueDeclaration &&
|
|
35
|
+
ts.isVariableDeclaration(symbol.valueDeclaration)
|
|
36
|
+
) {
|
|
37
|
+
const init = symbol.valueDeclaration.initializer
|
|
38
|
+
if (init) {
|
|
39
|
+
return extractStringLiteral(init, checker)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error('Unable to extract string literal from node')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if node is string-like (string literal or template expression)
|
|
49
|
+
*/
|
|
50
|
+
export function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean {
|
|
51
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
// Check if it's a template string with substitutions
|
|
55
|
+
if (ts.isTemplateExpression(node)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if node is function-like (arrow, function expression, or function declaration)
|
|
63
|
+
*/
|
|
64
|
+
export function isFunctionLike(node: ts.Node): boolean {
|
|
65
|
+
return (
|
|
66
|
+
ts.isArrowFunction(node) ||
|
|
67
|
+
ts.isFunctionExpression(node) ||
|
|
68
|
+
ts.isFunctionDeclaration(node)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract number literal value from a node
|
|
74
|
+
*/
|
|
75
|
+
export function extractNumberLiteral(node: ts.Node): number | null {
|
|
76
|
+
if (ts.isNumericLiteral(node)) {
|
|
77
|
+
return Number(node.text)
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract a property value from an object literal expression
|
|
84
|
+
* Returns the extracted value or null if not found/cannot extract
|
|
85
|
+
*/
|
|
86
|
+
export function extractPropertyString(
|
|
87
|
+
objNode: ts.ObjectLiteralExpression,
|
|
88
|
+
propertyName: string,
|
|
89
|
+
checker: ts.TypeChecker
|
|
90
|
+
): string | null {
|
|
91
|
+
for (const prop of objNode.properties) {
|
|
92
|
+
if (
|
|
93
|
+
ts.isPropertyAssignment(prop) &&
|
|
94
|
+
ts.isIdentifier(prop.name) &&
|
|
95
|
+
prop.name.text === propertyName
|
|
96
|
+
) {
|
|
97
|
+
return extractStringLiteral(prop.initializer, checker)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MiddlewareMetadata,
|
|
5
5
|
PermissionMetadata,
|
|
6
6
|
} from '@pikku/core'
|
|
7
|
+
import { extractTypeKeys } from './type-utils.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Helper to extract wire-level middleware/permission names from metadata.
|
|
@@ -28,7 +29,7 @@ export function extractWireNames(
|
|
|
28
29
|
*/
|
|
29
30
|
function expandAndAddGroupServices(
|
|
30
31
|
list: MiddlewareMetadata[] | PermissionMetadata[] | undefined,
|
|
31
|
-
state: InspectorState,
|
|
32
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>,
|
|
32
33
|
addServices: (services: FunctionServicesMeta | undefined) => void,
|
|
33
34
|
isMiddleware: boolean
|
|
34
35
|
): void {
|
|
@@ -57,6 +58,38 @@ function expandAndAddGroupServices(
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Extracts all service names from SingletonServices and Services types.
|
|
63
|
+
* This provides the complete list of available services for code generation.
|
|
64
|
+
* Only runs if typesLookup is available (omitted in deserialized states).
|
|
65
|
+
*/
|
|
66
|
+
function extractAllServices(
|
|
67
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
68
|
+
): void {
|
|
69
|
+
// Skip if typesLookup is not available (e.g., deserialized state)
|
|
70
|
+
if (!('typesLookup' in state)) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract all singleton services from the SingletonServices type
|
|
75
|
+
const singletonServicesTypes = state.typesLookup.get('SingletonServices')
|
|
76
|
+
if (singletonServicesTypes && singletonServicesTypes.length > 0) {
|
|
77
|
+
const singletonServiceNames = extractTypeKeys(singletonServicesTypes[0])
|
|
78
|
+
state.serviceAggregation.allSingletonServices = singletonServiceNames.sort()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract all services from the Services type
|
|
82
|
+
const servicesTypes = state.typesLookup.get('Services')
|
|
83
|
+
if (servicesTypes && servicesTypes.length > 0) {
|
|
84
|
+
const allServiceNames = extractTypeKeys(servicesTypes[0])
|
|
85
|
+
// Session services are those in Services but not in SingletonServices
|
|
86
|
+
const singletonSet = new Set(state.serviceAggregation.allSingletonServices)
|
|
87
|
+
state.serviceAggregation.allSessionServices = allServiceNames
|
|
88
|
+
.filter((name) => !singletonSet.has(name))
|
|
89
|
+
.sort()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
/**
|
|
61
94
|
* Aggregates all required services from wired functions, middleware, and permissions.
|
|
62
95
|
* Must be called after AST traversal completes.
|
|
@@ -64,7 +97,12 @@ function expandAndAddGroupServices(
|
|
|
64
97
|
* Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
|
|
65
98
|
* in the add-* methods during AST traversal for efficiency.
|
|
66
99
|
*/
|
|
67
|
-
export function aggregateRequiredServices(
|
|
100
|
+
export function aggregateRequiredServices(
|
|
101
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
102
|
+
): void {
|
|
103
|
+
// First, extract all available services from types
|
|
104
|
+
extractAllServices(state)
|
|
105
|
+
|
|
68
106
|
const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } =
|
|
69
107
|
state.serviceAggregation
|
|
70
108
|
|
|
@@ -112,6 +112,10 @@ export interface SerializableInspectorState {
|
|
|
112
112
|
meta: InspectorState['queueWorkers']['meta']
|
|
113
113
|
files: string[]
|
|
114
114
|
}
|
|
115
|
+
workflows: {
|
|
116
|
+
meta: InspectorState['workflows']['meta']
|
|
117
|
+
files: string[]
|
|
118
|
+
}
|
|
115
119
|
rpc: {
|
|
116
120
|
internalMeta: InspectorState['rpc']['internalMeta']
|
|
117
121
|
internalFiles: Array<[string, { path: string; exportedName: string }]>
|
|
@@ -162,6 +166,8 @@ export interface SerializableInspectorState {
|
|
|
162
166
|
usedFunctions: string[]
|
|
163
167
|
usedMiddleware: string[]
|
|
164
168
|
usedPermissions: string[]
|
|
169
|
+
allSingletonServices: string[]
|
|
170
|
+
allSessionServices: string[]
|
|
165
171
|
}
|
|
166
172
|
}
|
|
167
173
|
|
|
@@ -241,6 +247,10 @@ export function serializeInspectorState(
|
|
|
241
247
|
meta: state.queueWorkers.meta,
|
|
242
248
|
files: Array.from(state.queueWorkers.files),
|
|
243
249
|
},
|
|
250
|
+
workflows: {
|
|
251
|
+
meta: state.workflows.meta,
|
|
252
|
+
files: Array.from(state.workflows.files),
|
|
253
|
+
},
|
|
244
254
|
rpc: {
|
|
245
255
|
internalMeta: state.rpc.internalMeta,
|
|
246
256
|
internalFiles: Array.from(state.rpc.internalFiles.entries()),
|
|
@@ -271,6 +281,8 @@ export function serializeInspectorState(
|
|
|
271
281
|
usedFunctions: Array.from(state.serviceAggregation.usedFunctions),
|
|
272
282
|
usedMiddleware: Array.from(state.serviceAggregation.usedMiddleware),
|
|
273
283
|
usedPermissions: Array.from(state.serviceAggregation.usedPermissions),
|
|
284
|
+
allSingletonServices: state.serviceAggregation.allSingletonServices,
|
|
285
|
+
allSessionServices: state.serviceAggregation.allSessionServices,
|
|
274
286
|
},
|
|
275
287
|
}
|
|
276
288
|
}
|
|
@@ -340,6 +352,10 @@ export function deserializeInspectorState(
|
|
|
340
352
|
meta: data.queueWorkers.meta,
|
|
341
353
|
files: new Set(data.queueWorkers.files),
|
|
342
354
|
},
|
|
355
|
+
workflows: {
|
|
356
|
+
meta: data.workflows.meta,
|
|
357
|
+
files: new Set(data.workflows.files),
|
|
358
|
+
},
|
|
343
359
|
rpc: {
|
|
344
360
|
internalMeta: data.rpc.internalMeta,
|
|
345
361
|
internalFiles: new Map(data.rpc.internalFiles),
|
|
@@ -370,6 +386,8 @@ export function deserializeInspectorState(
|
|
|
370
386
|
usedFunctions: new Set(data.serviceAggregation.usedFunctions),
|
|
371
387
|
usedMiddleware: new Set(data.serviceAggregation.usedMiddleware),
|
|
372
388
|
usedPermissions: new Set(data.serviceAggregation.usedPermissions),
|
|
389
|
+
allSingletonServices: data.serviceAggregation.allSingletonServices,
|
|
390
|
+
allSessionServices: data.serviceAggregation.allSessionServices,
|
|
373
391
|
},
|
|
374
392
|
}
|
|
375
393
|
}
|