@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,290 @@
1
+ import * as ts from 'typescript';
2
+ import { extractFunctionName, isNamedExport, } from '../utils/extract-function-name.js';
3
+ import { extractServicesFromFunction } from '../utils/extract-services.js';
4
+ import { extractMiddlewarePikkuNames } from '../utils/middleware.js';
5
+ import { getPropertyValue } from '../utils/get-property-value.js';
6
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
+ /**
8
+ * Inspect pikkuMiddleware calls, addMiddleware calls, and addHTTPMiddleware calls
9
+ */
10
+ export const addMiddleware = (logger, node, checker, state) => {
11
+ if (!ts.isCallExpression(node))
12
+ return;
13
+ const { expression, arguments: args } = node;
14
+ // only handle specific function calls
15
+ if (!ts.isIdentifier(expression)) {
16
+ return;
17
+ }
18
+ // Handle pikkuMiddleware(...) - individual middleware function definition
19
+ if (expression.text === 'pikkuMiddleware') {
20
+ const arg = args[0];
21
+ if (!arg)
22
+ return;
23
+ let actualHandler;
24
+ let name;
25
+ let description;
26
+ // Check if using object syntax: pikkuMiddleware({ func: ..., name: '...', description: '...' })
27
+ if (ts.isObjectLiteralExpression(arg)) {
28
+ // Extract name and description metadata
29
+ const nameValue = getPropertyValue(arg, 'name');
30
+ const descValue = getPropertyValue(arg, 'description');
31
+ name = typeof nameValue === 'string' ? nameValue : undefined;
32
+ description = typeof descValue === 'string' ? descValue : undefined;
33
+ // Extract the func property
34
+ const fnProp = getPropertyAssignmentInitializer(arg, 'func', true, checker);
35
+ if (!fnProp ||
36
+ (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))) {
37
+ logger.error(`• pikkuMiddleware object missing required 'func' property.`);
38
+ return;
39
+ }
40
+ actualHandler = fnProp;
41
+ }
42
+ else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
43
+ actualHandler = arg;
44
+ }
45
+ else {
46
+ logger.error(`• Handler for pikkuMiddleware is not a function.`);
47
+ return;
48
+ }
49
+ const services = extractServicesFromFunction(actualHandler);
50
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker, state.rootDir);
51
+ state.middleware.meta[pikkuFuncName] = {
52
+ services,
53
+ sourceFile: node.getSourceFile().fileName,
54
+ position: node.getStart(),
55
+ exportedName,
56
+ name,
57
+ description,
58
+ };
59
+ logger.debug(`• Found middleware with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
60
+ return;
61
+ }
62
+ // Handle pikkuMiddlewareFactory(...) - middleware factory function
63
+ if (expression.text === 'pikkuMiddlewareFactory') {
64
+ const factoryNode = args[0];
65
+ if (!factoryNode)
66
+ return;
67
+ if (!ts.isArrowFunction(factoryNode) &&
68
+ !ts.isFunctionExpression(factoryNode)) {
69
+ logger.error(`• Handler for pikkuMiddlewareFactory is not a function.`);
70
+ return;
71
+ }
72
+ // Extract services by looking inside the factory function body
73
+ // The factory should return pikkuMiddleware(...), so we need to find that call
74
+ // If no wrapper is found, extract from the factory's returned function directly
75
+ let services = { optimized: false, services: [] };
76
+ const findPikkuMiddlewareCall = (node) => {
77
+ if (ts.isCallExpression(node)) {
78
+ const expr = node.expression;
79
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuMiddleware') {
80
+ return node;
81
+ }
82
+ }
83
+ return ts.forEachChild(node, findPikkuMiddlewareCall);
84
+ };
85
+ const pikkuMiddlewareCall = findPikkuMiddlewareCall(factoryNode);
86
+ if (pikkuMiddlewareCall && pikkuMiddlewareCall.arguments[0]) {
87
+ const middlewareHandler = pikkuMiddlewareCall.arguments[0];
88
+ if (ts.isArrowFunction(middlewareHandler) ||
89
+ ts.isFunctionExpression(middlewareHandler)) {
90
+ services = extractServicesFromFunction(middlewareHandler);
91
+ }
92
+ }
93
+ else {
94
+ // No pikkuMiddleware wrapper found - extract from factory's return value directly
95
+ // Factory pattern: (config) => (services, interaction, next) => { ... }
96
+ if (ts.isArrowFunction(factoryNode) ||
97
+ ts.isFunctionExpression(factoryNode)) {
98
+ const factoryBody = factoryNode.body;
99
+ // Check if the body is an arrow function (direct return)
100
+ if (ts.isArrowFunction(factoryBody) ||
101
+ ts.isFunctionExpression(factoryBody)) {
102
+ services = extractServicesFromFunction(factoryBody);
103
+ }
104
+ }
105
+ }
106
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker, state.rootDir);
107
+ state.middleware.meta[pikkuFuncName] = {
108
+ services,
109
+ sourceFile: node.getSourceFile().fileName,
110
+ position: node.getStart(),
111
+ exportedName,
112
+ factory: true,
113
+ };
114
+ logger.debug(`• Found middleware factory with services: ${services.services.join(', ')}`);
115
+ return;
116
+ }
117
+ // Handle addMiddleware('tag', [middleware1, middleware2])
118
+ // Supports two patterns:
119
+ // 1. export const x = () => addMiddleware('tag', [...]) (factory - tree-shakeable)
120
+ // 2. export const x = addMiddleware('tag', [...]) (direct - no tree-shaking)
121
+ if (expression.text === 'addMiddleware') {
122
+ const tagArg = args[0];
123
+ const middlewareArrayArg = args[1];
124
+ if (!tagArg || !middlewareArrayArg)
125
+ return;
126
+ // Extract tag name
127
+ let tag;
128
+ if (ts.isStringLiteral(tagArg)) {
129
+ tag = tagArg.text;
130
+ }
131
+ if (!tag) {
132
+ logger.warn(`• addMiddleware call without valid tag string`);
133
+ return;
134
+ }
135
+ // Check if middleware array is a literal array
136
+ if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
137
+ logger.error(`• addMiddleware('${tag}', ...) must have a literal array as second argument`);
138
+ return;
139
+ }
140
+ // Extract middleware pikkuFuncNames from array
141
+ const middlewareNames = extractMiddlewarePikkuNames(middlewareArrayArg, checker, state.rootDir);
142
+ if (middlewareNames.length === 0) {
143
+ logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`);
144
+ return;
145
+ }
146
+ // Collect services from all middleware in the group
147
+ const allServices = new Set();
148
+ for (const middlewareName of middlewareNames) {
149
+ const middlewareMeta = state.middleware.meta[middlewareName];
150
+ if (middlewareMeta && middlewareMeta.services) {
151
+ for (const service of middlewareMeta.services.services) {
152
+ allServices.add(service);
153
+ }
154
+ }
155
+ }
156
+ // Check if this call is wrapped in a factory function
157
+ // We need to walk up the tree to see if the parent is: const x = () => addMiddleware(...)
158
+ let isFactory = false;
159
+ let exportedName = null;
160
+ let parent = node.parent;
161
+ // Check if parent is arrow function: () => addMiddleware(...)
162
+ if (parent && ts.isArrowFunction(parent)) {
163
+ // Check if arrow function has no parameters
164
+ if (parent.parameters.length === 0) {
165
+ isFactory = true;
166
+ // For factories, we need to check the arrow function's parent for the export name
167
+ // const apiTagMiddleware = () => addMiddleware(...)
168
+ const arrowParent = parent.parent;
169
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
170
+ if (ts.isIdentifier(arrowParent.name)) {
171
+ // Check if it's exported
172
+ if (isNamedExport(arrowParent)) {
173
+ exportedName = arrowParent.name.text;
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ // If not a factory, get export name from the call expression itself
180
+ if (!isFactory) {
181
+ const extracted = extractFunctionName(node, checker, state.rootDir);
182
+ exportedName = extracted.exportedName;
183
+ }
184
+ // Log warning if not using factory pattern
185
+ if (!isFactory && exportedName) {
186
+ logger.warn(`• Middleware group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
187
+ `For tree-shaking, use: export const ${exportedName} = () => addMiddleware('${tag}', [...])`);
188
+ }
189
+ // Store group metadata
190
+ state.middleware.tagMiddleware.set(tag, {
191
+ exportName: exportedName,
192
+ sourceFile: node.getSourceFile().fileName,
193
+ position: node.getStart(),
194
+ services: {
195
+ optimized: false,
196
+ services: Array.from(allServices),
197
+ },
198
+ middlewareCount: middlewareNames.length,
199
+ isFactory,
200
+ });
201
+ logger.debug(`• Found tag middleware group: ${tag} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
202
+ return;
203
+ }
204
+ // Handle addHTTPMiddleware(pattern, [middleware1, middleware2])
205
+ // Supports two patterns:
206
+ // 1. export const x = () => addHTTPMiddleware('*', [...]) (factory - tree-shakeable)
207
+ // 2. export const x = addHTTPMiddleware('*', [...]) (direct - no tree-shaking)
208
+ if (expression.text === 'addHTTPMiddleware') {
209
+ const patternArg = args[0];
210
+ const middlewareArrayArg = args[1];
211
+ if (!patternArg || !middlewareArrayArg)
212
+ return;
213
+ // Extract route pattern
214
+ let pattern;
215
+ if (ts.isStringLiteral(patternArg)) {
216
+ pattern = patternArg.text;
217
+ }
218
+ if (!pattern) {
219
+ logger.warn(`• addHTTPMiddleware call without valid pattern string`);
220
+ return;
221
+ }
222
+ // Check if middleware array is a literal array
223
+ if (!ts.isArrayLiteralExpression(middlewareArrayArg)) {
224
+ logger.error(`• addHTTPMiddleware('${pattern}', ...) must have a literal array as second argument`);
225
+ return;
226
+ }
227
+ // Extract middleware pikkuFuncNames from array
228
+ const middlewareNames = extractMiddlewarePikkuNames(middlewareArrayArg, checker, state.rootDir);
229
+ if (middlewareNames.length === 0) {
230
+ logger.warn(`• addHTTPMiddleware('${pattern}', ...) has empty middleware array`);
231
+ return;
232
+ }
233
+ // Collect services from all middleware in the group
234
+ const allServices = new Set();
235
+ for (const middlewareName of middlewareNames) {
236
+ const middlewareMeta = state.middleware.meta[middlewareName];
237
+ if (middlewareMeta && middlewareMeta.services) {
238
+ for (const service of middlewareMeta.services.services) {
239
+ allServices.add(service);
240
+ }
241
+ }
242
+ }
243
+ // Check if this call is wrapped in a factory function
244
+ let isFactory = false;
245
+ let exportedName = null;
246
+ let parent = node.parent;
247
+ // Check if parent is arrow function: () => addHTTPMiddleware(...)
248
+ if (parent && ts.isArrowFunction(parent)) {
249
+ // Check if arrow function has no parameters
250
+ if (parent.parameters.length === 0) {
251
+ isFactory = true;
252
+ // For factories, we need to check the arrow function's parent for the export name
253
+ // const apiRouteMiddleware = () => addHTTPMiddleware(...)
254
+ const arrowParent = parent.parent;
255
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
256
+ if (ts.isIdentifier(arrowParent.name)) {
257
+ // Check if it's exported
258
+ if (isNamedExport(arrowParent)) {
259
+ exportedName = arrowParent.name.text;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ // If not a factory, get export name from the call expression itself
266
+ if (!isFactory) {
267
+ const extracted = extractFunctionName(node, checker, state.rootDir);
268
+ exportedName = extracted.exportedName;
269
+ }
270
+ // Log warning if not using factory pattern
271
+ if (!isFactory && exportedName) {
272
+ logger.warn(`• HTTP middleware group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
273
+ `For tree-shaking, use: export const ${exportedName} = () => addHTTPMiddleware('${pattern}', [...])`);
274
+ }
275
+ // Store group metadata
276
+ state.http.routeMiddleware.set(pattern, {
277
+ exportName: exportedName,
278
+ sourceFile: node.getSourceFile().fileName,
279
+ position: node.getStart(),
280
+ services: {
281
+ optimized: false,
282
+ services: Array.from(allServices),
283
+ },
284
+ middlewareCount: middlewareNames.length,
285
+ isFactory,
286
+ });
287
+ logger.debug(`• Found HTTP route middleware group: ${pattern} -> [${middlewareNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
288
+ return;
289
+ }
290
+ };
@@ -0,0 +1,5 @@
1
+ import { AddWiring } from '../types.js';
2
+ /**
3
+ * Inspect pikkuPermission calls, addPermission calls, and addHTTPPermission calls
4
+ */
5
+ export declare const addPermission: AddWiring;
@@ -0,0 +1,292 @@
1
+ import * as ts from 'typescript';
2
+ import { extractFunctionName, isNamedExport, } from '../utils/extract-function-name.js';
3
+ import { extractServicesFromFunction } from '../utils/extract-services.js';
4
+ import { extractPermissionPikkuNames } from '../utils/permissions.js';
5
+ import { getPropertyValue } from '../utils/get-property-value.js';
6
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
+ /**
8
+ * Inspect pikkuPermission calls, addPermission calls, and addHTTPPermission calls
9
+ */
10
+ export const addPermission = (logger, node, checker, state) => {
11
+ if (!ts.isCallExpression(node))
12
+ return;
13
+ const { expression, arguments: args } = node;
14
+ // only handle specific function calls
15
+ if (!ts.isIdentifier(expression)) {
16
+ return;
17
+ }
18
+ // Handle pikkuPermission(...) - individual permission function definition
19
+ if (expression.text === 'pikkuPermission') {
20
+ const arg = args[0];
21
+ if (!arg)
22
+ return;
23
+ let actualHandler;
24
+ let name;
25
+ let description;
26
+ // Check if using object syntax: pikkuPermission({ func: ..., name: '...', description: '...' })
27
+ if (ts.isObjectLiteralExpression(arg)) {
28
+ // Extract name and description metadata
29
+ const nameValue = getPropertyValue(arg, 'name');
30
+ const descValue = getPropertyValue(arg, 'description');
31
+ name = typeof nameValue === 'string' ? nameValue : undefined;
32
+ description = typeof descValue === 'string' ? descValue : undefined;
33
+ // Extract the func property
34
+ const fnProp = getPropertyAssignmentInitializer(arg, 'func', true, checker);
35
+ if (!fnProp ||
36
+ (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))) {
37
+ logger.error(`• pikkuPermission object missing required 'func' property.`);
38
+ return;
39
+ }
40
+ actualHandler = fnProp;
41
+ }
42
+ else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
43
+ actualHandler = arg;
44
+ }
45
+ else {
46
+ logger.error(`• Handler for pikkuPermission is not a function.`);
47
+ return;
48
+ }
49
+ const services = extractServicesFromFunction(actualHandler);
50
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker, state.rootDir);
51
+ state.permissions.meta[pikkuFuncName] = {
52
+ services,
53
+ sourceFile: node.getSourceFile().fileName,
54
+ position: node.getStart(),
55
+ exportedName,
56
+ name,
57
+ description,
58
+ };
59
+ logger.debug(`• Found permission with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
60
+ return;
61
+ }
62
+ // Handle pikkuPermissionFactory(...) - permission factory function
63
+ if (expression.text === 'pikkuPermissionFactory') {
64
+ const factoryNode = args[0];
65
+ if (!factoryNode)
66
+ return;
67
+ if (!ts.isArrowFunction(factoryNode) &&
68
+ !ts.isFunctionExpression(factoryNode)) {
69
+ logger.error(`• Handler for pikkuPermissionFactory is not a function.`);
70
+ return;
71
+ }
72
+ // Extract services by looking inside the factory function body
73
+ // The factory should return pikkuPermission(...), so we need to find that call
74
+ // If no wrapper is found, extract from the factory's returned function directly
75
+ let services = { optimized: false, services: [] };
76
+ const findPikkuPermissionCall = (node) => {
77
+ if (ts.isCallExpression(node)) {
78
+ const expr = node.expression;
79
+ if (ts.isIdentifier(expr) && expr.text === 'pikkuPermission') {
80
+ return node;
81
+ }
82
+ }
83
+ return ts.forEachChild(node, findPikkuPermissionCall);
84
+ };
85
+ const pikkuPermissionCall = findPikkuPermissionCall(factoryNode);
86
+ if (pikkuPermissionCall && pikkuPermissionCall.arguments[0]) {
87
+ const permissionHandler = pikkuPermissionCall.arguments[0];
88
+ if (ts.isArrowFunction(permissionHandler) ||
89
+ ts.isFunctionExpression(permissionHandler)) {
90
+ services = extractServicesFromFunction(permissionHandler);
91
+ }
92
+ }
93
+ else {
94
+ // No pikkuPermission wrapper found - extract from factory's return value directly
95
+ // Factory pattern: (config) => (services, data, session) => { ... }
96
+ if (ts.isArrowFunction(factoryNode) ||
97
+ ts.isFunctionExpression(factoryNode)) {
98
+ const factoryBody = factoryNode.body;
99
+ // Check if the body is an arrow function (direct return)
100
+ if (ts.isArrowFunction(factoryBody) ||
101
+ ts.isFunctionExpression(factoryBody)) {
102
+ services = extractServicesFromFunction(factoryBody);
103
+ }
104
+ }
105
+ }
106
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker, state.rootDir);
107
+ state.permissions.meta[pikkuFuncName] = {
108
+ services,
109
+ sourceFile: node.getSourceFile().fileName,
110
+ position: node.getStart(),
111
+ exportedName,
112
+ factory: true,
113
+ };
114
+ logger.debug(`• Found permission factory with services: ${services.services.join(', ')}`);
115
+ return;
116
+ }
117
+ // Handle addPermission('tag', [permission1, permission2])
118
+ // Supports two patterns:
119
+ // 1. export const x = () => addPermission('tag', [...]) (factory - tree-shakeable)
120
+ // 2. export const x = addPermission('tag', [...]) (direct - no tree-shaking)
121
+ if (expression.text === 'addPermission') {
122
+ const tagArg = args[0];
123
+ const permissionsArrayArg = args[1];
124
+ if (!tagArg || !permissionsArrayArg)
125
+ return;
126
+ // Extract tag name
127
+ let tag;
128
+ if (ts.isStringLiteral(tagArg)) {
129
+ tag = tagArg.text;
130
+ }
131
+ if (!tag) {
132
+ logger.warn(`• addPermission call without valid tag string`);
133
+ return;
134
+ }
135
+ // Check if permissions is a literal array or object
136
+ if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
137
+ !ts.isObjectLiteralExpression(permissionsArrayArg)) {
138
+ logger.error(`• addPermission('${tag}', ...) must have a literal array or object as second argument`);
139
+ return;
140
+ }
141
+ // Extract permission pikkuFuncNames from array
142
+ const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
143
+ if (permissionNames.length === 0) {
144
+ logger.warn(`• addPermission('${tag}', ...) has empty permissions array`);
145
+ return;
146
+ }
147
+ // Collect services from all permissions in the group
148
+ const allServices = new Set();
149
+ for (const permissionName of permissionNames) {
150
+ const permissionMeta = state.permissions.meta[permissionName];
151
+ if (permissionMeta && permissionMeta.services) {
152
+ for (const service of permissionMeta.services.services) {
153
+ allServices.add(service);
154
+ }
155
+ }
156
+ }
157
+ // Check if this call is wrapped in a factory function
158
+ // We need to walk up the tree to see if the parent is: const x = () => addPermission(...)
159
+ let isFactory = false;
160
+ let exportedName = null;
161
+ let parent = node.parent;
162
+ // Check if parent is arrow function: () => addPermission(...)
163
+ if (parent && ts.isArrowFunction(parent)) {
164
+ // Check if arrow function has no parameters
165
+ if (parent.parameters.length === 0) {
166
+ isFactory = true;
167
+ // For factories, we need to check the arrow function's parent for the export name
168
+ // const apiTagPermissions = () => addPermission(...)
169
+ const arrowParent = parent.parent;
170
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
171
+ if (ts.isIdentifier(arrowParent.name)) {
172
+ // Check if it's exported
173
+ if (isNamedExport(arrowParent)) {
174
+ exportedName = arrowParent.name.text;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ // If not a factory, get export name from the call expression itself
181
+ if (!isFactory) {
182
+ const extracted = extractFunctionName(node, checker, state.rootDir);
183
+ exportedName = extracted.exportedName;
184
+ }
185
+ // Log warning if not using factory pattern
186
+ if (!isFactory && exportedName) {
187
+ logger.warn(`• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
188
+ `For tree-shaking, use: export const ${exportedName} = () => addPermission('${tag}', [...])`);
189
+ }
190
+ // Store group metadata
191
+ state.permissions.tagPermissions.set(tag, {
192
+ exportName: exportedName,
193
+ sourceFile: node.getSourceFile().fileName,
194
+ position: node.getStart(),
195
+ services: {
196
+ optimized: false,
197
+ services: Array.from(allServices),
198
+ },
199
+ permissionCount: permissionNames.length,
200
+ isFactory,
201
+ });
202
+ logger.debug(`• Found tag permission group: ${tag} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
203
+ return;
204
+ }
205
+ // Handle addHTTPPermission(pattern, [permission1, permission2])
206
+ // Supports two patterns:
207
+ // 1. export const x = () => addHTTPPermission('*', [...]) (factory - tree-shakeable)
208
+ // 2. export const x = addHTTPPermission('*', [...]) (direct - no tree-shaking)
209
+ if (expression.text === 'addHTTPPermission') {
210
+ const patternArg = args[0];
211
+ const permissionsArrayArg = args[1];
212
+ if (!patternArg || !permissionsArrayArg)
213
+ return;
214
+ // Extract route pattern
215
+ let pattern;
216
+ if (ts.isStringLiteral(patternArg)) {
217
+ pattern = patternArg.text;
218
+ }
219
+ if (!pattern) {
220
+ logger.warn(`• addHTTPPermission call without valid pattern string`);
221
+ return;
222
+ }
223
+ // Check if permissions is a literal array or object
224
+ if (!ts.isArrayLiteralExpression(permissionsArrayArg) &&
225
+ !ts.isObjectLiteralExpression(permissionsArrayArg)) {
226
+ logger.error(`• addHTTPPermission('${pattern}', ...) must have a literal array or object as second argument`);
227
+ return;
228
+ }
229
+ // Extract permission pikkuFuncNames from array
230
+ const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
231
+ if (permissionNames.length === 0) {
232
+ logger.warn(`• addHTTPPermission('${pattern}', ...) has empty permissions array`);
233
+ return;
234
+ }
235
+ // Collect services from all permissions in the group
236
+ const allServices = new Set();
237
+ for (const permissionName of permissionNames) {
238
+ const permissionMeta = state.permissions.meta[permissionName];
239
+ if (permissionMeta && permissionMeta.services) {
240
+ for (const service of permissionMeta.services.services) {
241
+ allServices.add(service);
242
+ }
243
+ }
244
+ }
245
+ // Check if this call is wrapped in a factory function
246
+ let isFactory = false;
247
+ let exportedName = null;
248
+ let parent = node.parent;
249
+ // Check if parent is arrow function: () => addHTTPPermission(...)
250
+ if (parent && ts.isArrowFunction(parent)) {
251
+ // Check if arrow function has no parameters
252
+ if (parent.parameters.length === 0) {
253
+ isFactory = true;
254
+ // For factories, we need to check the arrow function's parent for the export name
255
+ // const apiRoutePermissions = () => addHTTPPermission(...)
256
+ const arrowParent = parent.parent;
257
+ if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
258
+ if (ts.isIdentifier(arrowParent.name)) {
259
+ // Check if it's exported
260
+ if (isNamedExport(arrowParent)) {
261
+ exportedName = arrowParent.name.text;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ // If not a factory, get export name from the call expression itself
268
+ if (!isFactory) {
269
+ const extracted = extractFunctionName(node, checker, state.rootDir);
270
+ exportedName = extracted.exportedName;
271
+ }
272
+ // Log warning if not using factory pattern
273
+ if (!isFactory && exportedName) {
274
+ logger.warn(`• HTTP permission group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
275
+ `For tree-shaking, use: export const ${exportedName} = () => addHTTPPermission('${pattern}', [...])`);
276
+ }
277
+ // Store group metadata
278
+ state.http.routePermissions.set(pattern, {
279
+ exportName: exportedName,
280
+ sourceFile: node.getSourceFile().fileName,
281
+ position: node.getStart(),
282
+ services: {
283
+ optimized: false,
284
+ services: Array.from(allServices),
285
+ },
286
+ permissionCount: permissionNames.length,
287
+ isFactory,
288
+ });
289
+ logger.debug(`• Found HTTP route permission group: ${pattern} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
290
+ return;
291
+ }
292
+ };
@@ -0,0 +1,2 @@
1
+ import { AddWiring } from '../types.js';
2
+ export declare const addQueueWorker: AddWiring;
@@ -0,0 +1,52 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue, getPropertyTags, } from '../utils/get-property-value.js';
3
+ import { extractFunctionName } from '../utils/extract-function-name.js';
4
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
5
+ import { resolveMiddleware } from '../utils/middleware.js';
6
+ import { extractWireNames } from '../utils/post-process.js';
7
+ import { ErrorCode } from '../error-codes.js';
8
+ export const addQueueWorker = (logger, node, checker, state, options) => {
9
+ if (!ts.isCallExpression(node)) {
10
+ return;
11
+ }
12
+ const args = node.arguments;
13
+ const firstArg = args[0];
14
+ const expression = node.expression;
15
+ // Check if the call is to addQueueWorker
16
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireQueueWorker') {
17
+ return;
18
+ }
19
+ if (!firstArg) {
20
+ return;
21
+ }
22
+ if (ts.isObjectLiteralExpression(firstArg)) {
23
+ const obj = firstArg;
24
+ const queueName = getPropertyValue(obj, 'queueName');
25
+ const docs = getPropertyValue(obj, 'docs') || undefined;
26
+ const tags = getPropertyTags(obj, 'Queue worker', queueName, logger);
27
+ // --- find the referenced function ---
28
+ const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
29
+ if (!funcInitializer) {
30
+ logger.critical(ErrorCode.MISSING_FUNC, `No valid 'func' property for queue processor '${queueName}'.`);
31
+ return;
32
+ }
33
+ const pikkuFuncName = extractFunctionName(funcInitializer, checker, state.rootDir).pikkuFuncName;
34
+ if (!queueName) {
35
+ logger.critical(ErrorCode.MISSING_QUEUE_NAME, `No 'queueName' provided for queue processor function '${pikkuFuncName}'.`);
36
+ return;
37
+ }
38
+ // --- resolve middleware ---
39
+ const middleware = resolveMiddleware(state, obj, tags, checker);
40
+ // --- track used functions/middleware for service aggregation ---
41
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName);
42
+ extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
43
+ state.queueWorkers.files.add(node.getSourceFile().fileName);
44
+ state.queueWorkers.meta[queueName] = {
45
+ pikkuFuncName,
46
+ queueName,
47
+ docs,
48
+ tags,
49
+ middleware,
50
+ };
51
+ }
52
+ };
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { InspectorState, InspectorLogger } from './types.js';
2
+ import { InspectorState, InspectorLogger } from '../types.js';
3
3
  /**
4
4
  * Scan for rpc.invoke() calls to track which functions are actually being invoked
5
5
  */
@@ -0,0 +1,2 @@
1
+ import { AddWiring } from '../types.js';
2
+ export declare const addSchedule: AddWiring;