@pikku/inspector 0.9.5 → 0.10.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 (133) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/add/add-channel.d.ts +17 -0
  3. package/dist/{add-channel.js → add/add-channel.js} +60 -34
  4. package/dist/add/add-cli.d.ts +9 -0
  5. package/dist/add/add-cli.js +566 -0
  6. package/dist/{add-file-extends-core-type.d.ts → add/add-file-extends-core-type.d.ts} +2 -2
  7. package/dist/{add-file-extends-core-type.js → add/add-file-extends-core-type.js} +17 -4
  8. package/dist/{add-file-with-config.d.ts → add/add-file-with-config.d.ts} +1 -1
  9. package/dist/{add-file-with-config.js → add/add-file-with-config.js} +1 -1
  10. package/dist/{add-file-with-factory.d.ts → add/add-file-with-factory.d.ts} +2 -2
  11. package/dist/{add-file-with-factory.js → add/add-file-with-factory.js} +38 -5
  12. package/dist/add/add-functions.d.ts +6 -0
  13. package/dist/{add-functions.js → add/add-functions.js} +77 -10
  14. package/dist/{add-http-route.d.ts → add/add-http-route.d.ts} +2 -3
  15. package/dist/{add-http-route.js → add/add-http-route.js} +26 -13
  16. package/dist/add/add-mcp-prompt.d.ts +2 -0
  17. package/dist/add/add-mcp-prompt.js +74 -0
  18. package/dist/add/add-mcp-resource.d.ts +2 -0
  19. package/dist/add/add-mcp-resource.js +84 -0
  20. package/dist/add/add-mcp-tool.d.ts +2 -0
  21. package/dist/add/add-mcp-tool.js +80 -0
  22. package/dist/add/add-middleware.d.ts +5 -0
  23. package/dist/add/add-middleware.js +290 -0
  24. package/dist/add/add-permission.d.ts +5 -0
  25. package/dist/add/add-permission.js +292 -0
  26. package/dist/add/add-queue-worker.d.ts +2 -0
  27. package/dist/add/add-queue-worker.js +52 -0
  28. package/dist/{add-rpc-invocations.d.ts → add/add-rpc-invocations.d.ts} +1 -1
  29. package/dist/add/add-schedule.d.ts +2 -0
  30. package/dist/{add-schedule.js → add/add-schedule.js} +16 -11
  31. package/dist/error-codes.d.ts +35 -0
  32. package/dist/error-codes.js +40 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.js +4 -0
  35. package/dist/inspector.d.ts +2 -3
  36. package/dist/inspector.js +38 -8
  37. package/dist/types.d.ts +108 -1
  38. package/dist/utils/ensure-function-metadata.d.ts +6 -0
  39. package/dist/utils/ensure-function-metadata.js +18 -0
  40. package/dist/utils/extract-function-name.d.ts +31 -0
  41. package/dist/{utils.js → utils/extract-function-name.js} +35 -149
  42. package/dist/utils/extract-services.d.ts +6 -0
  43. package/dist/utils/extract-services.js +29 -0
  44. package/dist/utils/filter-inspector-state.d.ts +6 -0
  45. package/dist/utils/filter-inspector-state.js +382 -0
  46. package/dist/utils/filter-utils.d.ts +19 -0
  47. package/dist/utils/filter-utils.js +109 -0
  48. package/dist/utils/find-root-dir.d.ts +23 -0
  49. package/dist/utils/find-root-dir.js +55 -0
  50. package/dist/utils/get-files-and-methods.d.ts +22 -0
  51. package/dist/utils/get-files-and-methods.js +61 -0
  52. package/dist/utils/get-property-value.d.ts +12 -0
  53. package/dist/{get-property-value.js → utils/get-property-value.js} +20 -0
  54. package/dist/utils/middleware.d.ts +39 -0
  55. package/dist/utils/middleware.js +157 -0
  56. package/dist/utils/permissions.d.ts +43 -0
  57. package/dist/utils/permissions.js +178 -0
  58. package/dist/utils/post-process.d.ts +16 -0
  59. package/dist/utils/post-process.js +132 -0
  60. package/dist/utils/serialize-inspector-state.d.ts +179 -0
  61. package/dist/utils/serialize-inspector-state.js +170 -0
  62. package/dist/utils/type-utils.d.ts +3 -0
  63. package/dist/utils/type-utils.js +50 -0
  64. package/dist/visit.d.ts +3 -3
  65. package/dist/visit.js +35 -31
  66. package/package.json +5 -6
  67. package/src/{add-channel.ts → add/add-channel.ts} +108 -56
  68. package/src/add/add-cli.ts +822 -0
  69. package/src/{add-file-extends-core-type.ts → add/add-file-extends-core-type.ts} +23 -5
  70. package/src/{add-file-with-config.ts → add/add-file-with-config.ts} +2 -2
  71. package/src/{add-file-with-factory.ts → add/add-file-with-factory.ts} +49 -6
  72. package/src/{add-functions.ts → add/add-functions.ts} +89 -19
  73. package/src/{add-http-route.ts → add/add-http-route.ts} +66 -32
  74. package/src/add/add-mcp-prompt.ts +128 -0
  75. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  76. package/src/add/add-mcp-resource.ts +145 -0
  77. package/src/add/add-mcp-resource.ts.tmp +0 -0
  78. package/src/add/add-mcp-tool.ts +137 -0
  79. package/src/add/add-middleware.ts +385 -0
  80. package/src/add/add-permission.ts +391 -0
  81. package/src/add/add-queue-worker.ts +92 -0
  82. package/src/{add-rpc-invocations.ts → add/add-rpc-invocations.ts} +1 -1
  83. package/src/{add-schedule.ts → add/add-schedule.ts} +30 -28
  84. package/src/error-codes.ts +43 -0
  85. package/src/index.ts +12 -0
  86. package/src/inspector.ts +41 -17
  87. package/src/types.ts +128 -1
  88. package/src/utils/ensure-function-metadata.ts +24 -0
  89. package/src/{utils.ts → utils/extract-function-name.ts} +44 -206
  90. package/src/utils/extract-services.ts +35 -0
  91. package/src/utils/filter-inspector-state.test.ts +1433 -0
  92. package/src/utils/filter-inspector-state.ts +526 -0
  93. package/src/{utils.test.ts → utils/filter-utils.test.ts} +351 -2
  94. package/src/utils/filter-utils.ts +152 -0
  95. package/src/utils/find-root-dir.ts +68 -0
  96. package/src/utils/get-files-and-methods.ts +151 -0
  97. package/src/{get-property-value.ts → utils/get-property-value.ts} +27 -0
  98. package/src/utils/middleware.ts +241 -0
  99. package/src/utils/permissions.test.ts +327 -0
  100. package/src/utils/permissions.ts +262 -0
  101. package/src/utils/post-process.ts +178 -0
  102. package/src/utils/serialize-inspector-state.ts +375 -0
  103. package/src/utils/test-data/inspector-state.json +1680 -0
  104. package/src/utils/type-utils.ts +74 -0
  105. package/src/visit.ts +50 -34
  106. package/tsconfig.tsbuildinfo +1 -1
  107. package/dist/add-channel.d.ts +0 -13
  108. package/dist/add-functions.d.ts +0 -7
  109. package/dist/add-mcp-prompt.d.ts +0 -3
  110. package/dist/add-mcp-prompt.js +0 -61
  111. package/dist/add-mcp-resource.d.ts +0 -3
  112. package/dist/add-mcp-resource.js +0 -68
  113. package/dist/add-mcp-tool.d.ts +0 -3
  114. package/dist/add-mcp-tool.js +0 -64
  115. package/dist/add-middleware.d.ts +0 -7
  116. package/dist/add-middleware.js +0 -35
  117. package/dist/add-permission.d.ts +0 -7
  118. package/dist/add-permission.js +0 -35
  119. package/dist/add-queue-worker.d.ts +0 -3
  120. package/dist/add-queue-worker.js +0 -48
  121. package/dist/add-schedule.d.ts +0 -3
  122. package/dist/get-property-value.d.ts +0 -3
  123. package/dist/utils.d.ts +0 -39
  124. package/src/add-mcp-prompt.ts +0 -104
  125. package/src/add-mcp-resource.ts +0 -116
  126. package/src/add-mcp-tool.ts +0 -107
  127. package/src/add-middleware.ts +0 -51
  128. package/src/add-permission.ts +0 -53
  129. package/src/add-queue-worker.ts +0 -92
  130. /package/dist/{add-rpc-invocations.js → add/add-rpc-invocations.js} +0 -0
  131. /package/dist/{does-type-extend-core-type.d.ts → utils/does-type-extend-core-type.d.ts} +0 -0
  132. /package/dist/{does-type-extend-core-type.js → utils/does-type-extend-core-type.js} +0 -0
  133. /package/src/{does-type-extend-core-type.ts → utils/does-type-extend-core-type.ts} +0 -0
@@ -0,0 +1,145 @@
1
+ import * as ts from 'typescript'
2
+ import {
3
+ getPropertyValue,
4
+ getPropertyTags,
5
+ } from '../utils/get-property-value.js'
6
+ import { extractWireNames } from '../utils/post-process.js'
7
+ import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
8
+ import { AddWiring } from '../types.js'
9
+ import { extractFunctionName } from '../utils/extract-function-name.js'
10
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
11
+ import { resolveMiddleware } from '../utils/middleware.js'
12
+ import { resolvePermissions } from '../utils/permissions.js'
13
+ import { ErrorCode } from '../error-codes.js'
14
+
15
+ export const addMCPResource: AddWiring = (
16
+ logger,
17
+ node,
18
+ checker,
19
+ state,
20
+ options
21
+ ) => {
22
+ if (!ts.isCallExpression(node)) {
23
+ return
24
+ }
25
+
26
+ const args = node.arguments
27
+ const firstArg = args[0]
28
+ const expression = node.expression
29
+
30
+ // Check if the call is to wireMCPResource
31
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireMCPResource') {
32
+ return
33
+ }
34
+
35
+ if (!firstArg) {
36
+ return
37
+ }
38
+
39
+ if (ts.isObjectLiteralExpression(firstArg)) {
40
+ const obj = firstArg
41
+
42
+ const uriValue = getPropertyValue(obj, 'uri') as string | null
43
+ const titleValue = getPropertyValue(obj, 'title') as string | null
44
+ const descriptionValue = getPropertyValue(obj, 'description') as
45
+ | string
46
+ | null
47
+ const streamingValue = getPropertyValue(obj, 'streaming') as boolean | null
48
+ const tags = getPropertyTags(obj, 'MCP resource', uriValue, logger)
49
+
50
+ if (streamingValue === true) {
51
+ logger.warn(
52
+ `MCP resource '${uriValue}' has streaming enabled, but streaming is not yet supported.`
53
+ )
54
+ }
55
+
56
+ const funcInitializer = getPropertyAssignmentInitializer(
57
+ obj,
58
+ 'func',
59
+ true,
60
+ checker
61
+ )
62
+ if (!funcInitializer) {
63
+ logger.critical(
64
+ ErrorCode.MISSING_FUNC,
65
+ `No valid 'func' property for MCP resource '${uriValue}'.`
66
+ )
67
+ return
68
+ }
69
+
70
+ const pikkuFuncName = extractFunctionName(
71
+ funcInitializer,
72
+ checker,
73
+ state.rootDir
74
+ ).pikkuFuncName
75
+
76
+ // Ensure function metadata exists (creates stub for inline functions)
77
+ ensureFunctionMetadata(state, pikkuFuncName, uriValue || undefined)
78
+
79
+ if (!uriValue) {
80
+ logger.critical(
81
+ ErrorCode.MISSING_URI,
82
+ "MCP resource is missing the required 'uri' property."
83
+ )
84
+ return
85
+ }
86
+
87
+ if (!titleValue) {
88
+ logger.critical(
89
+ ErrorCode.MISSING_TITLE,
90
+ `MCP resource '${uriValue}' is missing the required 'title' property.`
91
+ )
92
+ return
93
+ }
94
+
95
+ if (!descriptionValue) {
96
+ logger.critical(
97
+ ErrorCode.MISSING_DESCRIPTION,
98
+ `MCP resource '${uriValue}' is missing a description.`
99
+ )
100
+ return
101
+ }
102
+
103
+ // lookup existing function metadata
104
+ const fnMeta = state.functions.meta[pikkuFuncName]
105
+ if (!fnMeta) {
106
+ logger.critical(
107
+ ErrorCode.FUNCTION_METADATA_NOT_FOUND,
108
+ `No function metadata found for '${pikkuFuncName}'.`
109
+ )
110
+ return
111
+ }
112
+ const inputSchema = fnMeta.inputs?.[0] || null
113
+ const outputSchema = fnMeta.outputs?.[0] || null
114
+
115
+ // --- resolve middleware ---
116
+ const middleware = resolveMiddleware(state, obj, tags, checker)
117
+
118
+ // --- resolve permissions ---
119
+ const permissions = resolvePermissions(state, obj, tags, checker)
120
+
121
+ // --- track used functions/middleware/permissions for service aggregation ---
122
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName)
123
+ extractWireNames(middleware).forEach((name) =>
124
+ state.serviceAggregation.usedMiddleware.add(name)
125
+ )
126
+ extractWireNames(permissions).forEach((name) =>
127
+ state.serviceAggregation.usedPermissions.add(name)
128
+ )
129
+
130
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
131
+
132
+ state.mcpEndpoints.resourcesMeta[uriValue] = {
133
+ pikkuFuncName,
134
+ uri: uriValue,
135
+ title: titleValue,
136
+ description: descriptionValue,
137
+ ...(streamingValue !== null && { streaming: streamingValue }),
138
+ tags,
139
+ inputSchema,
140
+ outputSchema,
141
+ middleware,
142
+ permissions,
143
+ }
144
+ }
145
+ }
File without changes
@@ -0,0 +1,137 @@
1
+ import * as ts from 'typescript'
2
+ import {
3
+ getPropertyValue,
4
+ getPropertyTags,
5
+ } from '../utils/get-property-value.js'
6
+ import { extractWireNames } from '../utils/post-process.js'
7
+ import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js'
8
+ import { AddWiring } from '../types.js'
9
+ import { extractFunctionName } from '../utils/extract-function-name.js'
10
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
11
+ import { resolveMiddleware } from '../utils/middleware.js'
12
+ import { resolvePermissions } from '../utils/permissions.js'
13
+ import { ErrorCode } from '../error-codes.js'
14
+
15
+ export const addMCPTool: AddWiring = (
16
+ logger,
17
+ node,
18
+ checker,
19
+ state,
20
+ options
21
+ ) => {
22
+ if (!ts.isCallExpression(node)) {
23
+ return
24
+ }
25
+
26
+ const args = node.arguments
27
+ const firstArg = args[0]
28
+ const expression = node.expression
29
+
30
+ // Check if the call is to wireMCPTool
31
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireMCPTool') {
32
+ return
33
+ }
34
+
35
+ if (!firstArg) {
36
+ return
37
+ }
38
+
39
+ if (ts.isObjectLiteralExpression(firstArg)) {
40
+ const obj = firstArg
41
+
42
+ const nameValue = getPropertyValue(obj, 'name') as string | null
43
+ const titleValue = getPropertyValue(obj, 'title') as string | null
44
+ const descriptionValue = getPropertyValue(obj, 'description') as
45
+ | string
46
+ | null
47
+ const streamingValue = getPropertyValue(obj, 'streaming') as boolean | null
48
+ const tags = getPropertyTags(obj, 'MCP tool', nameValue, logger)
49
+
50
+ if (streamingValue === true) {
51
+ logger.warn(
52
+ `MCP tool '${nameValue}' has streaming enabled, but streaming is not yet supported.`
53
+ )
54
+ }
55
+
56
+ const funcInitializer = getPropertyAssignmentInitializer(
57
+ obj,
58
+ 'func',
59
+ true,
60
+ checker
61
+ )
62
+ if (!funcInitializer) {
63
+ logger.critical(
64
+ ErrorCode.MISSING_FUNC,
65
+ `No valid 'func' property for MCP tool '${nameValue}'.`
66
+ )
67
+ return
68
+ }
69
+
70
+ const pikkuFuncName = extractFunctionName(
71
+ funcInitializer,
72
+ checker,
73
+ state.rootDir
74
+ ).pikkuFuncName
75
+
76
+ // Ensure function metadata exists (creates stub for inline functions)
77
+ ensureFunctionMetadata(state, pikkuFuncName, nameValue || undefined)
78
+
79
+ if (!nameValue) {
80
+ logger.critical(
81
+ ErrorCode.MISSING_NAME,
82
+ "MCP tool is missing the required 'name' property."
83
+ )
84
+ return
85
+ }
86
+
87
+ if (!descriptionValue) {
88
+ logger.critical(
89
+ ErrorCode.MISSING_DESCRIPTION,
90
+ `MCP tool '${nameValue}' is missing a description.`
91
+ )
92
+ return
93
+ }
94
+
95
+ // lookup existing function metadata
96
+ const fnMeta = state.functions.meta[pikkuFuncName]
97
+ if (!fnMeta) {
98
+ logger.critical(
99
+ ErrorCode.FUNCTION_METADATA_NOT_FOUND,
100
+ `No function metadata found for '${pikkuFuncName}'.`
101
+ )
102
+ return
103
+ }
104
+ const inputSchema = fnMeta.inputs?.[0] || null
105
+ const outputSchema = fnMeta.outputs?.[0] || null
106
+
107
+ // --- resolve middleware ---
108
+ const middleware = resolveMiddleware(state, obj, tags, checker)
109
+
110
+ // --- resolve permissions ---
111
+ const permissions = resolvePermissions(state, obj, tags, checker)
112
+
113
+ // --- track used functions/middleware/permissions for service aggregation ---
114
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName)
115
+ extractWireNames(middleware).forEach((name) =>
116
+ state.serviceAggregation.usedMiddleware.add(name)
117
+ )
118
+ extractWireNames(permissions).forEach((name) =>
119
+ state.serviceAggregation.usedPermissions.add(name)
120
+ )
121
+
122
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
123
+
124
+ state.mcpEndpoints.toolsMeta[nameValue] = {
125
+ pikkuFuncName,
126
+ name: nameValue,
127
+ title: titleValue || undefined,
128
+ description: descriptionValue,
129
+ ...(streamingValue !== null && { streaming: streamingValue }),
130
+ tags,
131
+ inputSchema,
132
+ outputSchema,
133
+ middleware,
134
+ permissions,
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,385 @@
1
+ import * as ts from 'typescript'
2
+ import { AddWiring } from '../types.js'
3
+ import {
4
+ extractFunctionName,
5
+ isNamedExport,
6
+ } from '../utils/extract-function-name.js'
7
+ import { extractServicesFromFunction } from '../utils/extract-services.js'
8
+ import { extractMiddlewarePikkuNames } from '../utils/middleware.js'
9
+ import { getPropertyValue } from '../utils/get-property-value.js'
10
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
11
+
12
+ /**
13
+ * Inspect pikkuMiddleware calls, addMiddleware calls, and addHTTPMiddleware calls
14
+ */
15
+ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
16
+ if (!ts.isCallExpression(node)) return
17
+
18
+ const { expression, arguments: args } = node
19
+
20
+ // only handle specific function calls
21
+ if (!ts.isIdentifier(expression)) {
22
+ return
23
+ }
24
+
25
+ // Handle pikkuMiddleware(...) - individual middleware function definition
26
+ if (expression.text === 'pikkuMiddleware') {
27
+ const arg = args[0]
28
+ if (!arg) return
29
+
30
+ let actualHandler: ts.ArrowFunction | ts.FunctionExpression
31
+ let name: string | undefined
32
+ let description: string | undefined
33
+
34
+ // Check if using object syntax: pikkuMiddleware({ func: ..., name: '...', description: '...' })
35
+ if (ts.isObjectLiteralExpression(arg)) {
36
+ // Extract name and description metadata
37
+ const nameValue = getPropertyValue(arg, 'name')
38
+ const descValue = getPropertyValue(arg, 'description')
39
+ name = typeof nameValue === 'string' ? nameValue : undefined
40
+ description = typeof descValue === 'string' ? descValue : undefined
41
+
42
+ // Extract the func property
43
+ const fnProp = getPropertyAssignmentInitializer(
44
+ arg,
45
+ 'func',
46
+ true,
47
+ checker
48
+ )
49
+ if (
50
+ !fnProp ||
51
+ (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))
52
+ ) {
53
+ logger.error(
54
+ `• pikkuMiddleware object missing required 'func' property.`
55
+ )
56
+ return
57
+ }
58
+ actualHandler = fnProp
59
+ } else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
60
+ actualHandler = arg
61
+ } else {
62
+ logger.error(`• Handler for pikkuMiddleware is not a function.`)
63
+ return
64
+ }
65
+
66
+ const services = extractServicesFromFunction(actualHandler)
67
+ const { pikkuFuncName, exportedName } = extractFunctionName(
68
+ node,
69
+ checker,
70
+ state.rootDir
71
+ )
72
+ state.middleware.meta[pikkuFuncName] = {
73
+ services,
74
+ sourceFile: node.getSourceFile().fileName,
75
+ position: node.getStart(),
76
+ exportedName,
77
+ name,
78
+ description,
79
+ }
80
+
81
+ logger.debug(
82
+ `• Found middleware with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`
83
+ )
84
+ return
85
+ }
86
+
87
+ // Handle pikkuMiddlewareFactory(...) - middleware factory function
88
+ if (expression.text === 'pikkuMiddlewareFactory') {
89
+ const factoryNode = args[0]
90
+ if (!factoryNode) return
91
+
92
+ if (
93
+ !ts.isArrowFunction(factoryNode) &&
94
+ !ts.isFunctionExpression(factoryNode)
95
+ ) {
96
+ logger.error(`• Handler for pikkuMiddlewareFactory is not a function.`)
97
+ return
98
+ }
99
+
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
+ let services = { optimized: false, services: [] as string[] }
104
+
105
+ const findPikkuMiddlewareCall = (
106
+ node: ts.Node
107
+ ): ts.CallExpression | undefined => {
108
+ if (ts.isCallExpression(node)) {
109
+ const expr = node.expression
110
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuMiddleware') {
111
+ return node
112
+ }
113
+ }
114
+ return ts.forEachChild(node, findPikkuMiddlewareCall)
115
+ }
116
+
117
+ const pikkuMiddlewareCall = findPikkuMiddlewareCall(factoryNode)
118
+ if (pikkuMiddlewareCall && pikkuMiddlewareCall.arguments[0]) {
119
+ const middlewareHandler = pikkuMiddlewareCall.arguments[0]
120
+ if (
121
+ ts.isArrowFunction(middlewareHandler) ||
122
+ ts.isFunctionExpression(middlewareHandler)
123
+ ) {
124
+ services = extractServicesFromFunction(middlewareHandler)
125
+ }
126
+ } else {
127
+ // No pikkuMiddleware wrapper found - extract from factory's return value directly
128
+ // Factory pattern: (config) => (services, interaction, next) => { ... }
129
+ if (
130
+ ts.isArrowFunction(factoryNode) ||
131
+ ts.isFunctionExpression(factoryNode)
132
+ ) {
133
+ const factoryBody = factoryNode.body
134
+ // Check if the body is an arrow function (direct return)
135
+ if (
136
+ ts.isArrowFunction(factoryBody) ||
137
+ ts.isFunctionExpression(factoryBody)
138
+ ) {
139
+ services = extractServicesFromFunction(factoryBody)
140
+ }
141
+ }
142
+ }
143
+
144
+ const { pikkuFuncName, exportedName } = extractFunctionName(
145
+ node,
146
+ checker,
147
+ state.rootDir
148
+ )
149
+ state.middleware.meta[pikkuFuncName] = {
150
+ services,
151
+ sourceFile: node.getSourceFile().fileName,
152
+ position: node.getStart(),
153
+ exportedName,
154
+ factory: true,
155
+ }
156
+
157
+ logger.debug(
158
+ `• Found middleware factory with services: ${services.services.join(', ')}`
159
+ )
160
+ return
161
+ }
162
+
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
+ if (expression.text === 'addMiddleware') {
168
+ const tagArg = args[0]
169
+ const middlewareArrayArg = args[1]
170
+
171
+ if (!tagArg || !middlewareArrayArg) return
172
+
173
+ // Extract tag name
174
+ let tag: string | undefined
175
+ if (ts.isStringLiteral(tagArg)) {
176
+ tag = tagArg.text
177
+ }
178
+
179
+ if (!tag) {
180
+ logger.warn(`• addMiddleware call without valid tag string`)
181
+ return
182
+ }
183
+
184
+ // Check if middleware array is a literal array
185
+ if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
186
+ logger.error(
187
+ `• addMiddleware('${tag}', ...) must have a literal array as second argument`
188
+ )
189
+ return
190
+ }
191
+
192
+ // Extract middleware pikkuFuncNames from array
193
+ const middlewareNames = extractMiddlewarePikkuNames(
194
+ middlewareArrayArg,
195
+ checker,
196
+ state.rootDir
197
+ )
198
+
199
+ if (middlewareNames.length === 0) {
200
+ logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`)
201
+ return
202
+ }
203
+
204
+ // Collect services from all middleware in the group
205
+ 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) {
210
+ allServices.add(service)
211
+ }
212
+ }
213
+ }
214
+
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
+ let isFactory = false
218
+ let exportedName: string | null = null
219
+ let parent = node.parent
220
+
221
+ // Check if parent is arrow function: () => addMiddleware(...)
222
+ if (parent && ts.isArrowFunction(parent)) {
223
+ // Check if arrow function has no parameters
224
+ if (parent.parameters.length === 0) {
225
+ isFactory = true
226
+
227
+ // For factories, we need to check the arrow function's parent for the export name
228
+ // const apiTagMiddleware = () => addMiddleware(...)
229
+ const arrowParent = parent.parent
230
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
231
+ if (ts.isIdentifier(arrowParent.name)) {
232
+ // Check if it's exported
233
+ if (isNamedExport(arrowParent)) {
234
+ exportedName = arrowParent.name.text
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // If not a factory, get export name from the call expression itself
242
+ if (!isFactory) {
243
+ const extracted = extractFunctionName(node, checker, state.rootDir)
244
+ exportedName = extracted.exportedName
245
+ }
246
+
247
+ // Log warning if not using factory pattern
248
+ if (!isFactory && exportedName) {
249
+ logger.warn(
250
+ `• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
251
+ `For tree-shaking, use: export const ${exportedName} = () => addMiddleware('${tag}', [...])`
252
+ )
253
+ }
254
+
255
+ // Store group metadata
256
+ state.middleware.tagMiddleware.set(tag, {
257
+ exportName: exportedName,
258
+ sourceFile: node.getSourceFile().fileName,
259
+ position: node.getStart(),
260
+ services: {
261
+ optimized: false,
262
+ services: Array.from(allServices),
263
+ },
264
+ middlewareCount: middlewareNames.length,
265
+ isFactory,
266
+ })
267
+
268
+ logger.debug(
269
+ `• Found tag middleware group: ${tag} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
270
+ )
271
+ return
272
+ }
273
+
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
+ if (expression.text === 'addHTTPMiddleware') {
279
+ const patternArg = args[0]
280
+ const middlewareArrayArg = args[1]
281
+
282
+ if (!patternArg || !middlewareArrayArg) return
283
+
284
+ // Extract route pattern
285
+ let pattern: string | undefined
286
+ if (ts.isStringLiteral(patternArg)) {
287
+ pattern = patternArg.text
288
+ }
289
+
290
+ if (!pattern) {
291
+ logger.warn(`• addHTTPMiddleware call without valid pattern string`)
292
+ return
293
+ }
294
+
295
+ // Check if middleware array is a literal array
296
+ if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
297
+ logger.error(
298
+ `• addHTTPMiddleware('${pattern}', ...) must have a literal array as second argument`
299
+ )
300
+ return
301
+ }
302
+
303
+ // Extract middleware pikkuFuncNames from array
304
+ const middlewareNames = extractMiddlewarePikkuNames(
305
+ middlewareArrayArg,
306
+ checker,
307
+ state.rootDir
308
+ )
309
+
310
+ if (middlewareNames.length === 0) {
311
+ logger.warn(
312
+ `• addHTTPMiddleware('${pattern}', ...) has empty middleware array`
313
+ )
314
+ return
315
+ }
316
+
317
+ // Collect services from all middleware in the group
318
+ 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) {
323
+ allServices.add(service)
324
+ }
325
+ }
326
+ }
327
+
328
+ // Check if this call is wrapped in a factory function
329
+ let isFactory = false
330
+ let exportedName: string | null = null
331
+ let parent = node.parent
332
+
333
+ // Check if parent is arrow function: () => addHTTPMiddleware(...)
334
+ if (parent && ts.isArrowFunction(parent)) {
335
+ // Check if arrow function has no parameters
336
+ if (parent.parameters.length === 0) {
337
+ isFactory = true
338
+
339
+ // For factories, we need to check the arrow function's parent for the export name
340
+ // const apiRouteMiddleware = () => addHTTPMiddleware(...)
341
+ const arrowParent = parent.parent
342
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
343
+ if (ts.isIdentifier(arrowParent.name)) {
344
+ // Check if it's exported
345
+ if (isNamedExport(arrowParent)) {
346
+ exportedName = arrowParent.name.text
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ // If not a factory, get export name from the call expression itself
354
+ if (!isFactory) {
355
+ const extracted = extractFunctionName(node, checker, state.rootDir)
356
+ exportedName = extracted.exportedName
357
+ }
358
+
359
+ // Log warning if not using factory pattern
360
+ if (!isFactory && exportedName) {
361
+ logger.warn(
362
+ `• HTTP middleware group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
363
+ `For tree-shaking, use: export const ${exportedName} = () => addHTTPMiddleware('${pattern}', [...])`
364
+ )
365
+ }
366
+
367
+ // Store group metadata
368
+ state.http.routeMiddleware.set(pattern, {
369
+ exportName: exportedName,
370
+ sourceFile: node.getSourceFile().fileName,
371
+ position: node.getStart(),
372
+ services: {
373
+ optimized: false,
374
+ services: Array.from(allServices),
375
+ },
376
+ middlewareCount: middlewareNames.length,
377
+ isFactory,
378
+ })
379
+
380
+ logger.debug(
381
+ `• Found HTTP route middleware group: ${pattern} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
382
+ )
383
+ return
384
+ }
385
+ }