@pikku/inspector 0.11.1 → 0.12.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.
Files changed (189) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +327 -59
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +153 -44
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.d.ts +3 -0
  23. package/dist/add/add-rpc-invocations.js +65 -25
  24. package/dist/add/add-schedule.js +11 -5
  25. package/dist/add/add-secret.d.ts +3 -0
  26. package/dist/add/add-secret.js +82 -0
  27. package/dist/add/add-trigger.d.ts +2 -0
  28. package/dist/add/add-trigger.js +87 -0
  29. package/dist/add/add-variable.d.ts +1 -0
  30. package/dist/add/add-variable.js +8 -0
  31. package/dist/add/add-workflow-graph.d.ts +7 -0
  32. package/dist/add/add-workflow-graph.js +396 -0
  33. package/dist/add/add-workflow.js +124 -26
  34. package/dist/error-codes.d.ts +16 -1
  35. package/dist/error-codes.js +21 -1
  36. package/dist/index.d.ts +9 -5
  37. package/dist/index.js +5 -2
  38. package/dist/inspector.d.ts +1 -1
  39. package/dist/inspector.js +106 -13
  40. package/dist/schema-generator.d.ts +1 -0
  41. package/dist/schema-generator.js +1 -0
  42. package/dist/types-map.js +10 -1
  43. package/dist/types.d.ts +180 -30
  44. package/dist/utils/compute-required-schemas.d.ts +4 -0
  45. package/dist/utils/compute-required-schemas.js +41 -0
  46. package/dist/utils/contract-hashes.d.ts +35 -0
  47. package/dist/utils/contract-hashes.js +202 -0
  48. package/dist/utils/custom-types-generator.d.ts +9 -0
  49. package/dist/utils/custom-types-generator.js +71 -0
  50. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  51. package/dist/utils/detect-schema-vendor.js +76 -0
  52. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  53. package/dist/utils/ensure-function-metadata.js +220 -6
  54. package/dist/utils/extract-function-name.d.ts +5 -16
  55. package/dist/utils/extract-function-name.js +93 -298
  56. package/dist/utils/extract-services.d.ts +2 -1
  57. package/dist/utils/extract-services.js +25 -1
  58. package/dist/utils/filter-inspector-state.js +107 -23
  59. package/dist/utils/get-property-value.d.ts +8 -2
  60. package/dist/utils/get-property-value.js +33 -4
  61. package/dist/utils/hash.d.ts +2 -0
  62. package/dist/utils/hash.js +23 -0
  63. package/dist/utils/middleware.d.ts +7 -30
  64. package/dist/utils/middleware.js +80 -66
  65. package/dist/utils/permissions.d.ts +2 -2
  66. package/dist/utils/permissions.js +10 -10
  67. package/dist/utils/post-process.d.ts +9 -10
  68. package/dist/utils/post-process.js +231 -24
  69. package/dist/utils/resolve-external-package.d.ts +12 -0
  70. package/dist/utils/resolve-external-package.js +34 -0
  71. package/dist/utils/resolve-function-types.d.ts +6 -0
  72. package/dist/utils/resolve-function-types.js +29 -0
  73. package/dist/utils/resolve-identifier.d.ts +10 -0
  74. package/dist/utils/resolve-identifier.js +36 -0
  75. package/dist/utils/resolve-versions.d.ts +2 -0
  76. package/dist/utils/resolve-versions.js +78 -0
  77. package/dist/utils/schema-generator.d.ts +9 -0
  78. package/dist/utils/schema-generator.js +209 -0
  79. package/dist/utils/serialize-inspector-state.d.ts +73 -13
  80. package/dist/utils/serialize-inspector-state.js +102 -6
  81. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  82. package/dist/utils/serialize-mcp-json.js +99 -0
  83. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  84. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  85. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  86. package/dist/utils/serialize-openapi-json.js +151 -0
  87. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  88. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  89. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
  90. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
  91. package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
  92. package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
  93. package/dist/utils/workflow/dsl/index.d.ts +7 -0
  94. package/dist/utils/workflow/dsl/index.js +7 -0
  95. package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
  96. package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
  97. package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
  98. package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
  99. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
  100. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
  101. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  102. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  103. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  104. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  105. package/dist/utils/workflow/graph/index.d.ts +8 -0
  106. package/dist/utils/workflow/graph/index.js +8 -0
  107. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
  108. package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
  109. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
  110. package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
  111. package/dist/visit.js +13 -2
  112. package/package.json +26 -4
  113. package/src/add/add-ai-agent.ts +468 -0
  114. package/src/add/add-channel.ts +82 -79
  115. package/src/add/add-cli.ts +49 -20
  116. package/src/add/add-file-with-factory.ts +2 -0
  117. package/src/add/add-functions.ts +429 -71
  118. package/src/add/add-http-route.ts +246 -65
  119. package/src/add/add-http-routes.ts +228 -0
  120. package/src/add/add-keyed-wiring.ts +151 -0
  121. package/src/add/add-mcp-prompt.ts +26 -15
  122. package/src/add/add-mcp-resource.ts +27 -15
  123. package/src/add/add-middleware.ts +482 -80
  124. package/src/add/add-permission.ts +199 -40
  125. package/src/add/add-queue-worker.ts +24 -19
  126. package/src/add/add-rpc-invocations.ts +78 -31
  127. package/src/add/add-schedule.ts +16 -11
  128. package/src/add/add-secret.ts +140 -0
  129. package/src/add/add-trigger.ts +154 -0
  130. package/src/add/add-variable.ts +9 -0
  131. package/src/add/add-workflow-graph.ts +522 -0
  132. package/src/add/add-workflow.ts +117 -30
  133. package/src/error-codes.ts +26 -1
  134. package/src/index.ts +27 -8
  135. package/src/inspector.ts +145 -17
  136. package/src/schema-generator.ts +1 -0
  137. package/src/types-map.ts +12 -1
  138. package/src/types.ts +192 -51
  139. package/src/utils/compute-required-schemas.ts +49 -0
  140. package/src/utils/contract-hashes.test.ts +528 -0
  141. package/src/utils/contract-hashes.ts +290 -0
  142. package/src/utils/custom-types-generator.ts +88 -0
  143. package/src/utils/detect-schema-vendor.ts +90 -0
  144. package/src/utils/ensure-function-metadata.ts +324 -7
  145. package/src/utils/extract-function-name.ts +108 -358
  146. package/src/utils/extract-services.ts +35 -2
  147. package/src/utils/filter-inspector-state.test.ts +34 -20
  148. package/src/utils/filter-inspector-state.ts +140 -31
  149. package/src/utils/get-property-value.ts +50 -5
  150. package/src/utils/hash.ts +26 -0
  151. package/src/utils/middleware.test.ts +204 -0
  152. package/src/utils/middleware.ts +129 -67
  153. package/src/utils/permissions.test.ts +35 -12
  154. package/src/utils/permissions.ts +10 -10
  155. package/src/utils/post-process.ts +283 -43
  156. package/src/utils/resolve-external-package.ts +42 -0
  157. package/src/utils/resolve-function-types.ts +42 -0
  158. package/src/utils/resolve-identifier.ts +46 -0
  159. package/src/utils/resolve-versions.test.ts +249 -0
  160. package/src/utils/resolve-versions.ts +105 -0
  161. package/src/utils/schema-generator.ts +329 -0
  162. package/src/utils/serialize-inspector-state.ts +181 -20
  163. package/src/utils/serialize-mcp-json.ts +145 -0
  164. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  165. package/src/utils/serialize-openapi-json.ts +277 -0
  166. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  167. package/src/utils/test-data/inspector-state.json +69 -66
  168. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
  169. package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
  170. package/src/utils/workflow/dsl/index.ts +11 -0
  171. package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
  172. package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
  173. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
  174. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  175. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  176. package/src/utils/workflow/graph/index.ts +11 -0
  177. package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
  178. package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
  179. package/src/visit.ts +14 -2
  180. package/tsconfig.tsbuildinfo +1 -1
  181. package/dist/add/add-mcp-tool.d.ts +0 -2
  182. package/dist/add/add-mcp-tool.js +0 -81
  183. package/dist/utils/extract-service-metadata.d.ts +0 -19
  184. package/dist/utils/extract-service-metadata.js +0 -244
  185. package/dist/utils/write-service-metadata.d.ts +0 -13
  186. package/dist/utils/write-service-metadata.js +0 -37
  187. package/src/add/add-mcp-tool.ts +0 -141
  188. package/src/utils/extract-service-metadata.ts +0 -353
  189. package/src/utils/write-service-metadata.ts +0 -51
@@ -1,28 +1,53 @@
1
1
  import * as ts from 'typescript'
2
- import { AddWiring } from '../types.js'
2
+ import type { AddWiring, InspectorState } from '../types.js'
3
3
  import {
4
4
  extractFunctionName,
5
5
  isNamedExport,
6
+ makeContextBasedId,
6
7
  } from '../utils/extract-function-name.js'
7
- import { extractServicesFromFunction } from '../utils/extract-services.js'
8
- import { extractMiddlewarePikkuNames } from '../utils/middleware.js'
8
+ import {
9
+ extractServicesFromFunction,
10
+ extractUsedWires,
11
+ } from '../utils/extract-services.js'
12
+ import { extractMiddlewareRefs } from '../utils/middleware.js'
9
13
  import { getPropertyValue } from '../utils/get-property-value.js'
10
14
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
11
15
 
12
- /**
13
- * Inspect pikkuMiddleware calls, addMiddleware calls, and addHTTPMiddleware calls
14
- */
16
+ function renameTempDefinitions(
17
+ state: InspectorState,
18
+ definitionIds: string[],
19
+ groupType: string,
20
+ groupKey: string,
21
+ storeKey: 'middleware' | 'channelMiddleware' = 'middleware'
22
+ ): void {
23
+ const tempIndices = definitionIds
24
+ .map((name, i) => (name.startsWith('__temp_') ? i : -1))
25
+ .filter((i) => i >= 0)
26
+
27
+ for (const idx of tempIndices) {
28
+ const oldId = definitionIds[idx]
29
+ const newId =
30
+ tempIndices.length === 1
31
+ ? makeContextBasedId(groupType, groupKey)
32
+ : makeContextBasedId(groupType, groupKey, String(idx))
33
+ const existing = state[storeKey].definitions[oldId]
34
+ if (existing) {
35
+ delete state[storeKey].definitions[oldId]
36
+ state[storeKey].definitions[newId] = existing
37
+ }
38
+ definitionIds[idx] = newId
39
+ }
40
+ }
41
+
15
42
  export const addMiddleware: AddWiring = (logger, node, checker, state) => {
16
43
  if (!ts.isCallExpression(node)) return
17
44
 
18
45
  const { expression, arguments: args } = node
19
46
 
20
- // only handle specific function calls
21
47
  if (!ts.isIdentifier(expression)) {
22
48
  return
23
49
  }
24
50
 
25
- // Handle pikkuMiddleware(...) - individual middleware function definition
26
51
  if (expression.text === 'pikkuMiddleware') {
27
52
  const arg = args[0]
28
53
  if (!arg) return
@@ -31,15 +56,12 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
31
56
  let name: string | undefined
32
57
  let description: string | undefined
33
58
 
34
- // Check if using object syntax: pikkuMiddleware({ func: ..., name: '...', description: '...' })
35
59
  if (ts.isObjectLiteralExpression(arg)) {
36
- // Extract name and description metadata
37
60
  const nameValue = getPropertyValue(arg, 'name')
38
61
  const descValue = getPropertyValue(arg, 'description')
39
62
  name = typeof nameValue === 'string' ? nameValue : undefined
40
63
  description = typeof descValue === 'string' ? descValue : undefined
41
64
 
42
- // Extract the func property
43
65
  const fnProp = getPropertyAssignmentInitializer(
44
66
  arg,
45
67
  'func',
@@ -64,13 +86,34 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
64
86
  }
65
87
 
66
88
  const services = extractServicesFromFunction(actualHandler)
67
- const { pikkuFuncName, exportedName } = extractFunctionName(
89
+ const wires = extractUsedWires(actualHandler, 1)
90
+ let { pikkuFuncId, exportedName } = extractFunctionName(
68
91
  node,
69
92
  checker,
70
93
  state.rootDir
71
94
  )
72
- state.middleware.meta[pikkuFuncName] = {
95
+ if (pikkuFuncId.startsWith('__temp_')) {
96
+ if (
97
+ ts.isVariableDeclaration(node.parent) &&
98
+ ts.isIdentifier(node.parent.name)
99
+ ) {
100
+ pikkuFuncId = node.parent.name.text
101
+ } else if (
102
+ ts.isPropertyAssignment(node.parent) &&
103
+ ts.isIdentifier(node.parent.name)
104
+ ) {
105
+ pikkuFuncId = node.parent.name.text
106
+ } else {
107
+ logger.error(
108
+ `• pikkuMiddleware() must be assigned to a variable or object property. ` +
109
+ `Extract it to a const: const myMiddleware = pikkuMiddleware(...)`
110
+ )
111
+ return
112
+ }
113
+ }
114
+ state.middleware.definitions[pikkuFuncId] = {
73
115
  services,
116
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
74
117
  sourceFile: node.getSourceFile().fileName,
75
118
  position: node.getStart(),
76
119
  exportedName,
@@ -84,7 +127,6 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
84
127
  return
85
128
  }
86
129
 
87
- // Handle pikkuMiddlewareFactory(...) - middleware factory function
88
130
  if (expression.text === 'pikkuMiddlewareFactory') {
89
131
  const factoryNode = args[0]
90
132
  if (!factoryNode) return
@@ -97,10 +139,13 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
97
139
  return
98
140
  }
99
141
 
100
- // Extract services by looking inside the factory function body
101
- // The factory should return pikkuMiddleware(...), so we need to find that call
102
- // If no wrapper is found, extract from the factory's returned function directly
103
142
  let services = { optimized: false, services: [] as string[] }
143
+ let wires: ReturnType<typeof extractUsedWires> = {
144
+ optimized: true,
145
+ wires: [],
146
+ }
147
+ let name: string | undefined
148
+ let description: string | undefined
104
149
 
105
150
  const findPikkuMiddlewareCall = (
106
151
  node: ts.Node
@@ -116,61 +161,96 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
116
161
 
117
162
  const pikkuMiddlewareCall = findPikkuMiddlewareCall(factoryNode)
118
163
  if (pikkuMiddlewareCall && pikkuMiddlewareCall.arguments[0]) {
119
- const middlewareHandler = pikkuMiddlewareCall.arguments[0]
120
- if (
121
- ts.isArrowFunction(middlewareHandler) ||
122
- ts.isFunctionExpression(middlewareHandler)
164
+ const middlewareArg = pikkuMiddlewareCall.arguments[0]
165
+ if (ts.isObjectLiteralExpression(middlewareArg)) {
166
+ const nameValue = getPropertyValue(middlewareArg, 'name')
167
+ const descValue = getPropertyValue(middlewareArg, 'description')
168
+ name = typeof nameValue === 'string' ? nameValue : undefined
169
+ description = typeof descValue === 'string' ? descValue : undefined
170
+
171
+ const fnProp = getPropertyAssignmentInitializer(
172
+ middlewareArg,
173
+ 'func',
174
+ true,
175
+ checker
176
+ )
177
+ if (
178
+ fnProp &&
179
+ (ts.isArrowFunction(fnProp) || ts.isFunctionExpression(fnProp))
180
+ ) {
181
+ services = extractServicesFromFunction(fnProp)
182
+ wires = extractUsedWires(fnProp, 1)
183
+ }
184
+ } else if (
185
+ ts.isArrowFunction(middlewareArg) ||
186
+ ts.isFunctionExpression(middlewareArg)
123
187
  ) {
124
- services = extractServicesFromFunction(middlewareHandler)
188
+ services = extractServicesFromFunction(middlewareArg)
189
+ wires = extractUsedWires(middlewareArg, 1)
125
190
  }
126
191
  } else {
127
- // No pikkuMiddleware wrapper found - extract from factory's return value directly
128
- // Factory pattern: (config) => (services, wire, next) => { ... }
129
192
  if (
130
193
  ts.isArrowFunction(factoryNode) ||
131
194
  ts.isFunctionExpression(factoryNode)
132
195
  ) {
133
196
  const factoryBody = factoryNode.body
134
- // Check if the body is an arrow function (direct return)
135
197
  if (
136
198
  ts.isArrowFunction(factoryBody) ||
137
199
  ts.isFunctionExpression(factoryBody)
138
200
  ) {
139
201
  services = extractServicesFromFunction(factoryBody)
202
+ wires = extractUsedWires(factoryBody, 1)
140
203
  }
141
204
  }
142
205
  }
143
206
 
144
- const { pikkuFuncName, exportedName } = extractFunctionName(
207
+ let { pikkuFuncId, exportedName } = extractFunctionName(
145
208
  node,
146
209
  checker,
147
210
  state.rootDir
148
211
  )
149
- state.middleware.meta[pikkuFuncName] = {
212
+ if (pikkuFuncId.startsWith('__temp_')) {
213
+ if (
214
+ ts.isVariableDeclaration(node.parent) &&
215
+ ts.isIdentifier(node.parent.name)
216
+ ) {
217
+ pikkuFuncId = node.parent.name.text
218
+ } else if (
219
+ ts.isPropertyAssignment(node.parent) &&
220
+ ts.isIdentifier(node.parent.name)
221
+ ) {
222
+ pikkuFuncId = node.parent.name.text
223
+ } else {
224
+ logger.error(
225
+ `• pikkuMiddlewareFactory() must be assigned to a variable or object property. ` +
226
+ `Extract it to a const: const myMiddleware = pikkuMiddlewareFactory(...)`
227
+ )
228
+ return
229
+ }
230
+ }
231
+ state.middleware.definitions[pikkuFuncId] = {
150
232
  services,
233
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
151
234
  sourceFile: node.getSourceFile().fileName,
152
235
  position: node.getStart(),
153
236
  exportedName,
154
237
  factory: true,
238
+ name,
239
+ description,
155
240
  }
156
241
 
157
242
  logger.debug(
158
- `• Found middleware factory with services: ${services.services.join(', ')}`
243
+ `• Found middleware factory with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`
159
244
  )
160
245
  return
161
246
  }
162
247
 
163
- // Handle addMiddleware('tag', [middleware1, middleware2])
164
- // Supports two patterns:
165
- // 1. export const x = () => addMiddleware('tag', [...]) (factory - tree-shakeable)
166
- // 2. export const x = addMiddleware('tag', [...]) (direct - no tree-shaking)
167
248
  if (expression.text === 'addMiddleware') {
168
249
  const tagArg = args[0]
169
250
  const middlewareArrayArg = args[1]
170
251
 
171
252
  if (!tagArg || !middlewareArrayArg) return
172
253
 
173
- // Extract tag name
174
254
  let tag: string | undefined
175
255
  if (ts.isStringLiteral(tagArg)) {
176
256
  tag = tagArg.text
@@ -181,7 +261,6 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
181
261
  return
182
262
  }
183
263
 
184
- // Check if middleware array is a literal array
185
264
  if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
186
265
  logger.error(
187
266
  `• addMiddleware('${tag}', ...) must have a literal array as second argument`
@@ -189,47 +268,54 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
189
268
  return
190
269
  }
191
270
 
192
- // Extract middleware pikkuFuncNames from array
193
- const middlewareNames = extractMiddlewarePikkuNames(
271
+ const refs = extractMiddlewareRefs(
194
272
  middlewareArrayArg,
195
273
  checker,
196
274
  state.rootDir
197
275
  )
198
276
 
199
- if (middlewareNames.length === 0) {
277
+ if (refs.length === 0) {
200
278
  logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`)
201
279
  return
202
280
  }
203
281
 
204
- // Collect services from all middleware in the group
282
+ const definitionIds = refs.map((r) => r.definitionId)
283
+ renameTempDefinitions(state, definitionIds, 'tag', tag)
284
+
285
+ const sourceFile = node.getSourceFile().fileName
286
+ const instanceIds: string[] = []
287
+ for (let i = 0; i < refs.length; i++) {
288
+ const instanceId = makeContextBasedId('tag', tag, String(i))
289
+ state.middleware.instances[instanceId] = {
290
+ definitionId: definitionIds[i],
291
+ sourceFile,
292
+ position: node.getStart(),
293
+ isFactoryCall: refs[i].isFactoryCall,
294
+ }
295
+ instanceIds.push(instanceId)
296
+ }
297
+
205
298
  const allServices = new Set<string>()
206
- for (const middlewareName of middlewareNames) {
207
- const middlewareMeta = state.middleware.meta[middlewareName]
208
- if (middlewareMeta && middlewareMeta.services) {
209
- for (const service of middlewareMeta.services.services) {
299
+ for (const defId of definitionIds) {
300
+ const def = state.middleware.definitions[defId]
301
+ if (def?.services) {
302
+ for (const service of def.services.services) {
210
303
  allServices.add(service)
211
304
  }
212
305
  }
213
306
  }
214
307
 
215
- // Check if this call is wrapped in a factory function
216
- // We need to walk up the tree to see if the parent is: const x = () => addMiddleware(...)
217
308
  let isFactory = false
218
309
  let exportedName: string | null = null
219
310
  let parent = node.parent
220
311
 
221
- // Check if parent is arrow function: () => addMiddleware(...)
222
312
  if (parent && ts.isArrowFunction(parent)) {
223
- // Check if arrow function has no parameters
224
313
  if (parent.parameters.length === 0) {
225
314
  isFactory = true
226
315
 
227
- // For factories, we need to check the arrow function's parent for the export name
228
- // const apiTagMiddleware = () => addMiddleware(...)
229
316
  const arrowParent = parent.parent
230
317
  if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
231
318
  if (ts.isIdentifier(arrowParent.name)) {
232
- // Check if it's exported
233
319
  if (isNamedExport(arrowParent)) {
234
320
  exportedName = arrowParent.name.text
235
321
  }
@@ -238,13 +324,11 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
238
324
  }
239
325
  }
240
326
 
241
- // If not a factory, get export name from the call expression itself
242
327
  if (!isFactory) {
243
328
  const extracted = extractFunctionName(node, checker, state.rootDir)
244
329
  exportedName = extracted.exportedName
245
330
  }
246
331
 
247
- // Log warning if not using factory pattern
248
332
  if (!isFactory && exportedName) {
249
333
  logger.warn(
250
334
  `• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
@@ -252,36 +336,31 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
252
336
  )
253
337
  }
254
338
 
255
- // Store group metadata
256
339
  state.middleware.tagMiddleware.set(tag, {
257
340
  exportName: exportedName,
258
- sourceFile: node.getSourceFile().fileName,
341
+ sourceFile,
259
342
  position: node.getStart(),
260
343
  services: {
261
344
  optimized: false,
262
345
  services: Array.from(allServices),
263
346
  },
264
- middlewareCount: middlewareNames.length,
347
+ count: refs.length,
348
+ instanceIds,
265
349
  isFactory,
266
350
  })
267
351
 
268
352
  logger.debug(
269
- `• Found tag middleware group: ${tag} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
353
+ `• Found tag middleware group: ${tag} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
270
354
  )
271
355
  return
272
356
  }
273
357
 
274
- // Handle addHTTPMiddleware(pattern, [middleware1, middleware2])
275
- // Supports two patterns:
276
- // 1. export const x = () => addHTTPMiddleware('*', [...]) (factory - tree-shakeable)
277
- // 2. export const x = addHTTPMiddleware('*', [...]) (direct - no tree-shaking)
278
358
  if (expression.text === 'addHTTPMiddleware') {
279
359
  const patternArg = args[0]
280
360
  const middlewareArrayArg = args[1]
281
361
 
282
362
  if (!patternArg || !middlewareArrayArg) return
283
363
 
284
- // Extract route pattern
285
364
  let pattern: string | undefined
286
365
  if (ts.isStringLiteral(patternArg)) {
287
366
  pattern = patternArg.text
@@ -292,7 +371,6 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
292
371
  return
293
372
  }
294
373
 
295
- // Check if middleware array is a literal array
296
374
  if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
297
375
  logger.error(
298
376
  `• addHTTPMiddleware('${pattern}', ...) must have a literal array as second argument`
@@ -300,48 +378,56 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
300
378
  return
301
379
  }
302
380
 
303
- // Extract middleware pikkuFuncNames from array
304
- const middlewareNames = extractMiddlewarePikkuNames(
381
+ const refs = extractMiddlewareRefs(
305
382
  middlewareArrayArg,
306
383
  checker,
307
384
  state.rootDir
308
385
  )
309
386
 
310
- if (middlewareNames.length === 0) {
387
+ if (refs.length === 0) {
311
388
  logger.warn(
312
389
  `• addHTTPMiddleware('${pattern}', ...) has empty middleware array`
313
390
  )
314
391
  return
315
392
  }
316
393
 
317
- // Collect services from all middleware in the group
394
+ const definitionIds = refs.map((r) => r.definitionId)
395
+ renameTempDefinitions(state, definitionIds, 'http', pattern)
396
+
397
+ const sourceFile = node.getSourceFile().fileName
398
+ const instanceIds: string[] = []
399
+ for (let i = 0; i < refs.length; i++) {
400
+ const instanceId = makeContextBasedId('http', pattern, String(i))
401
+ state.middleware.instances[instanceId] = {
402
+ definitionId: definitionIds[i],
403
+ sourceFile,
404
+ position: node.getStart(),
405
+ isFactoryCall: refs[i].isFactoryCall,
406
+ }
407
+ instanceIds.push(instanceId)
408
+ }
409
+
318
410
  const allServices = new Set<string>()
319
- for (const middlewareName of middlewareNames) {
320
- const middlewareMeta = state.middleware.meta[middlewareName]
321
- if (middlewareMeta && middlewareMeta.services) {
322
- for (const service of middlewareMeta.services.services) {
411
+ for (const defId of definitionIds) {
412
+ const def = state.middleware.definitions[defId]
413
+ if (def?.services) {
414
+ for (const service of def.services.services) {
323
415
  allServices.add(service)
324
416
  }
325
417
  }
326
418
  }
327
419
 
328
- // Check if this call is wrapped in a factory function
329
420
  let isFactory = false
330
421
  let exportedName: string | null = null
331
422
  let parent = node.parent
332
423
 
333
- // Check if parent is arrow function: () => addHTTPMiddleware(...)
334
424
  if (parent && ts.isArrowFunction(parent)) {
335
- // Check if arrow function has no parameters
336
425
  if (parent.parameters.length === 0) {
337
426
  isFactory = true
338
427
 
339
- // For factories, we need to check the arrow function's parent for the export name
340
- // const apiRouteMiddleware = () => addHTTPMiddleware(...)
341
428
  const arrowParent = parent.parent
342
429
  if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
343
430
  if (ts.isIdentifier(arrowParent.name)) {
344
- // Check if it's exported
345
431
  if (isNamedExport(arrowParent)) {
346
432
  exportedName = arrowParent.name.text
347
433
  }
@@ -350,13 +436,11 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
350
436
  }
351
437
  }
352
438
 
353
- // If not a factory, get export name from the call expression itself
354
439
  if (!isFactory) {
355
440
  const extracted = extractFunctionName(node, checker, state.rootDir)
356
441
  exportedName = extracted.exportedName
357
442
  }
358
443
 
359
- // Log warning if not using factory pattern
360
444
  if (!isFactory && exportedName) {
361
445
  logger.warn(
362
446
  `• HTTP middleware group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
@@ -364,21 +448,339 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
364
448
  )
365
449
  }
366
450
 
367
- // Store group metadata
368
451
  state.http.routeMiddleware.set(pattern, {
369
452
  exportName: exportedName,
453
+ sourceFile,
454
+ position: node.getStart(),
455
+ services: {
456
+ optimized: false,
457
+ services: Array.from(allServices),
458
+ },
459
+ count: refs.length,
460
+ instanceIds,
461
+ isFactory,
462
+ })
463
+
464
+ logger.debug(
465
+ `• Found HTTP route middleware group: ${pattern} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
466
+ )
467
+ return
468
+ }
469
+
470
+ if (expression.text === 'pikkuChannelMiddleware') {
471
+ const arg = args[0]
472
+ if (!arg) return
473
+
474
+ let actualHandler: ts.ArrowFunction | ts.FunctionExpression
475
+
476
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
477
+ actualHandler = arg
478
+ } else {
479
+ logger.error(`• Handler for pikkuChannelMiddleware is not a function.`)
480
+ return
481
+ }
482
+
483
+ const services = extractServicesFromFunction(actualHandler)
484
+ let { pikkuFuncId, exportedName } = extractFunctionName(
485
+ node,
486
+ checker,
487
+ state.rootDir
488
+ )
489
+ if (pikkuFuncId.startsWith('__temp_')) {
490
+ if (
491
+ ts.isVariableDeclaration(node.parent) &&
492
+ ts.isIdentifier(node.parent.name)
493
+ ) {
494
+ pikkuFuncId = node.parent.name.text
495
+ } else if (
496
+ ts.isPropertyAssignment(node.parent) &&
497
+ ts.isIdentifier(node.parent.name)
498
+ ) {
499
+ pikkuFuncId = node.parent.name.text
500
+ } else {
501
+ logger.error(
502
+ `• pikkuChannelMiddleware() must be assigned to a variable or object property. ` +
503
+ `Extract it to a const: const myMiddleware = pikkuChannelMiddleware(...)`
504
+ )
505
+ return
506
+ }
507
+ }
508
+ state.channelMiddleware.definitions[pikkuFuncId] = {
509
+ services,
510
+ sourceFile: node.getSourceFile().fileName,
511
+ position: node.getStart(),
512
+ exportedName,
513
+ }
514
+
515
+ logger.debug(
516
+ `• Found channel middleware with services: ${services.services.join(', ')}`
517
+ )
518
+ return
519
+ }
520
+
521
+ if (expression.text === 'pikkuChannelMiddlewareFactory') {
522
+ const factoryNode = args[0]
523
+ if (!factoryNode) return
524
+
525
+ if (
526
+ !ts.isArrowFunction(factoryNode) &&
527
+ !ts.isFunctionExpression(factoryNode)
528
+ ) {
529
+ logger.error(
530
+ `• Handler for pikkuChannelMiddlewareFactory is not a function.`
531
+ )
532
+ return
533
+ }
534
+
535
+ let services = { optimized: false, services: [] as string[] }
536
+
537
+ const findPikkuChannelMiddlewareCall = (
538
+ n: ts.Node
539
+ ): ts.CallExpression | undefined => {
540
+ if (ts.isCallExpression(n)) {
541
+ const expr = n.expression
542
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuChannelMiddleware') {
543
+ return n
544
+ }
545
+ }
546
+ return ts.forEachChild(n, findPikkuChannelMiddlewareCall)
547
+ }
548
+
549
+ const channelMiddlewareCall = findPikkuChannelMiddlewareCall(factoryNode)
550
+ if (channelMiddlewareCall && channelMiddlewareCall.arguments[0]) {
551
+ const middlewareHandler = channelMiddlewareCall.arguments[0]
552
+ if (
553
+ ts.isArrowFunction(middlewareHandler) ||
554
+ ts.isFunctionExpression(middlewareHandler)
555
+ ) {
556
+ services = extractServicesFromFunction(middlewareHandler)
557
+ }
558
+ } else {
559
+ if (
560
+ ts.isArrowFunction(factoryNode) ||
561
+ ts.isFunctionExpression(factoryNode)
562
+ ) {
563
+ const factoryBody = factoryNode.body
564
+ if (
565
+ ts.isArrowFunction(factoryBody) ||
566
+ ts.isFunctionExpression(factoryBody)
567
+ ) {
568
+ services = extractServicesFromFunction(factoryBody)
569
+ }
570
+ }
571
+ }
572
+
573
+ let { pikkuFuncId, exportedName } = extractFunctionName(
574
+ node,
575
+ checker,
576
+ state.rootDir
577
+ )
578
+ if (pikkuFuncId.startsWith('__temp_')) {
579
+ if (
580
+ ts.isVariableDeclaration(node.parent) &&
581
+ ts.isIdentifier(node.parent.name)
582
+ ) {
583
+ pikkuFuncId = node.parent.name.text
584
+ } else if (
585
+ ts.isPropertyAssignment(node.parent) &&
586
+ ts.isIdentifier(node.parent.name)
587
+ ) {
588
+ pikkuFuncId = node.parent.name.text
589
+ } else {
590
+ logger.error(
591
+ `• pikkuChannelMiddlewareFactory() must be assigned to a variable or object property. ` +
592
+ `Extract it to a const: const myMiddleware = pikkuChannelMiddlewareFactory(...)`
593
+ )
594
+ return
595
+ }
596
+ }
597
+ state.channelMiddleware.definitions[pikkuFuncId] = {
598
+ services,
370
599
  sourceFile: node.getSourceFile().fileName,
371
600
  position: node.getStart(),
601
+ exportedName,
602
+ factory: true,
603
+ }
604
+
605
+ logger.debug(
606
+ `• Found channel middleware factory with services: ${services.services.join(', ')}`
607
+ )
608
+ return
609
+ }
610
+
611
+ if (expression.text === 'pikkuAIMiddleware') {
612
+ const arg = args[0]
613
+ if (!arg) return
614
+
615
+ if (!ts.isObjectLiteralExpression(arg)) {
616
+ logger.error(`• pikkuAIMiddleware() requires an object literal argument.`)
617
+ return
618
+ }
619
+
620
+ const allServices = new Set<string>()
621
+ for (const prop of arg.properties) {
622
+ if (
623
+ ts.isPropertyAssignment(prop) &&
624
+ (ts.isArrowFunction(prop.initializer) ||
625
+ ts.isFunctionExpression(prop.initializer))
626
+ ) {
627
+ const hookServices = extractServicesFromFunction(prop.initializer)
628
+ for (const s of hookServices.services) {
629
+ allServices.add(s)
630
+ }
631
+ }
632
+ }
633
+
634
+ const services = {
635
+ optimized: allServices.size > 0,
636
+ services: Array.from(allServices),
637
+ }
638
+
639
+ let { pikkuFuncId, exportedName } = extractFunctionName(
640
+ node,
641
+ checker,
642
+ state.rootDir
643
+ )
644
+ if (pikkuFuncId.startsWith('__temp_')) {
645
+ if (
646
+ ts.isVariableDeclaration(node.parent) &&
647
+ ts.isIdentifier(node.parent.name)
648
+ ) {
649
+ pikkuFuncId = node.parent.name.text
650
+ } else if (
651
+ ts.isPropertyAssignment(node.parent) &&
652
+ ts.isIdentifier(node.parent.name)
653
+ ) {
654
+ pikkuFuncId = node.parent.name.text
655
+ } else {
656
+ logger.error(
657
+ `• pikkuAIMiddleware() must be assigned to a variable or object property. ` +
658
+ `Extract it to a const: const myMiddleware = pikkuAIMiddleware(...)`
659
+ )
660
+ return
661
+ }
662
+ }
663
+ state.aiMiddleware.definitions[pikkuFuncId] = {
664
+ services,
665
+ sourceFile: node.getSourceFile().fileName,
666
+ position: node.getStart(),
667
+ exportedName,
668
+ }
669
+
670
+ logger.debug(
671
+ `• Found AI middleware with services: ${services.services.join(', ')}`
672
+ )
673
+ return
674
+ }
675
+
676
+ if (expression.text === 'addChannelMiddleware') {
677
+ const tagArg = args[0]
678
+ const middlewareArrayArg = args[1]
679
+
680
+ if (!tagArg || !middlewareArrayArg) return
681
+
682
+ let tag: string | undefined
683
+ if (ts.isStringLiteral(tagArg)) {
684
+ tag = tagArg.text
685
+ }
686
+
687
+ if (!tag) {
688
+ logger.warn(`• addChannelMiddleware call without valid tag string`)
689
+ return
690
+ }
691
+
692
+ if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
693
+ logger.error(
694
+ `• addChannelMiddleware('${tag}', ...) must have a literal array as second argument`
695
+ )
696
+ return
697
+ }
698
+
699
+ const refs = extractMiddlewareRefs(
700
+ middlewareArrayArg,
701
+ checker,
702
+ state.rootDir
703
+ )
704
+
705
+ if (refs.length === 0) {
706
+ logger.warn(
707
+ `• addChannelMiddleware('${tag}', ...) has empty middleware array`
708
+ )
709
+ return
710
+ }
711
+
712
+ const definitionIds = refs.map((r) => r.definitionId)
713
+ renameTempDefinitions(state, definitionIds, 'tag', tag, 'channelMiddleware')
714
+
715
+ const sourceFile = node.getSourceFile().fileName
716
+ const instanceIds: string[] = []
717
+ for (let i = 0; i < refs.length; i++) {
718
+ const instanceId = makeContextBasedId('tag', tag, String(i))
719
+ state.channelMiddleware.instances[instanceId] = {
720
+ definitionId: definitionIds[i],
721
+ sourceFile,
722
+ position: node.getStart(),
723
+ isFactoryCall: refs[i].isFactoryCall,
724
+ }
725
+ instanceIds.push(instanceId)
726
+ }
727
+
728
+ const allServices = new Set<string>()
729
+ for (const defId of definitionIds) {
730
+ const def = state.channelMiddleware.definitions[defId]
731
+ if (def?.services) {
732
+ for (const service of def.services.services) {
733
+ allServices.add(service)
734
+ }
735
+ }
736
+ }
737
+
738
+ let isFactory = false
739
+ let exportedName: string | null = null
740
+ let parent = node.parent
741
+
742
+ if (parent && ts.isArrowFunction(parent)) {
743
+ if (parent.parameters.length === 0) {
744
+ isFactory = true
745
+
746
+ const arrowParent = parent.parent
747
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
748
+ if (ts.isIdentifier(arrowParent.name)) {
749
+ if (isNamedExport(arrowParent)) {
750
+ exportedName = arrowParent.name.text
751
+ }
752
+ }
753
+ }
754
+ }
755
+ }
756
+
757
+ if (!isFactory) {
758
+ const extracted = extractFunctionName(node, checker, state.rootDir)
759
+ exportedName = extracted.exportedName
760
+ }
761
+
762
+ if (!isFactory && exportedName) {
763
+ logger.warn(
764
+ `• Channel middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
765
+ `For tree-shaking, use: export const ${exportedName} = () => addChannelMiddleware('${tag}', [...])`
766
+ )
767
+ }
768
+
769
+ state.channelMiddleware.tagMiddleware.set(tag, {
770
+ exportName: exportedName,
771
+ sourceFile,
772
+ position: node.getStart(),
372
773
  services: {
373
774
  optimized: false,
374
775
  services: Array.from(allServices),
375
776
  },
376
- middlewareCount: middlewareNames.length,
777
+ count: refs.length,
778
+ instanceIds,
377
779
  isFactory,
378
780
  })
379
781
 
380
782
  logger.debug(
381
- `• Found HTTP route middleware group: ${pattern} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
783
+ `• Found tag channel middleware group: ${tag} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
382
784
  )
383
785
  return
384
786
  }