@pikku/inspector 0.9.6-next.0 → 0.10.1

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 (84) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/add/add-channel.d.ts +5 -1
  3. package/dist/add/add-channel.js +51 -32
  4. package/dist/add/add-cli.d.ts +4 -0
  5. package/dist/add/add-cli.js +128 -23
  6. package/dist/add/add-file-extends-core-type.js +3 -2
  7. package/dist/add/add-file-with-factory.d.ts +2 -2
  8. package/dist/add/add-file-with-factory.js +87 -1
  9. package/dist/add/add-functions.js +52 -5
  10. package/dist/add/add-http-route.js +19 -12
  11. package/dist/add/add-mcp-prompt.js +20 -13
  12. package/dist/add/add-mcp-resource.js +24 -14
  13. package/dist/add/add-mcp-tool.js +23 -13
  14. package/dist/add/add-middleware.js +51 -12
  15. package/dist/add/add-permission.d.ts +1 -2
  16. package/dist/add/add-permission.js +275 -19
  17. package/dist/add/add-queue-worker.js +10 -12
  18. package/dist/add/add-schedule.js +9 -10
  19. package/dist/error-codes.d.ts +35 -0
  20. package/dist/error-codes.js +40 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/inspector.js +20 -1
  24. package/dist/types.d.ts +31 -3
  25. package/dist/utils/ensure-function-metadata.d.ts +6 -0
  26. package/dist/utils/ensure-function-metadata.js +18 -0
  27. package/dist/utils/extract-function-name.d.ts +2 -2
  28. package/dist/utils/extract-function-name.js +13 -8
  29. package/dist/utils/filter-inspector-state.d.ts +6 -0
  30. package/dist/utils/filter-inspector-state.js +382 -0
  31. package/dist/utils/filter-utils.d.ts +10 -0
  32. package/dist/utils/filter-utils.js +66 -2
  33. package/dist/utils/find-root-dir.d.ts +23 -0
  34. package/dist/utils/find-root-dir.js +55 -0
  35. package/dist/utils/get-files-and-methods.d.ts +2 -1
  36. package/dist/utils/get-files-and-methods.js +4 -3
  37. package/dist/utils/get-property-value.d.ts +9 -0
  38. package/dist/utils/get-property-value.js +20 -0
  39. package/dist/utils/middleware.d.ts +1 -1
  40. package/dist/utils/middleware.js +7 -7
  41. package/dist/utils/permissions.d.ts +43 -0
  42. package/dist/utils/permissions.js +178 -0
  43. package/dist/utils/post-process.d.ts +16 -0
  44. package/dist/utils/post-process.js +132 -0
  45. package/dist/utils/serialize-inspector-state.d.ts +179 -0
  46. package/dist/utils/serialize-inspector-state.js +170 -0
  47. package/dist/visit.js +3 -2
  48. package/package.json +4 -4
  49. package/src/add/add-channel.ts +92 -40
  50. package/src/add/add-cli.ts +188 -29
  51. package/src/add/add-file-extends-core-type.ts +5 -2
  52. package/src/add/add-file-with-factory.ts +114 -2
  53. package/src/add/add-functions.ts +60 -5
  54. package/src/add/add-http-route.ts +46 -21
  55. package/src/add/add-mcp-prompt.ts +42 -21
  56. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  57. package/src/add/add-mcp-resource.ts +50 -24
  58. package/src/add/add-mcp-resource.ts.tmp +0 -0
  59. package/src/add/add-mcp-tool.ts +48 -21
  60. package/src/add/add-middleware.ts +74 -15
  61. package/src/add/add-permission.ts +364 -22
  62. package/src/add/add-queue-worker.ts +22 -25
  63. package/src/add/add-schedule.ts +19 -20
  64. package/src/error-codes.ts +43 -0
  65. package/src/index.ts +7 -0
  66. package/src/inspector.ts +22 -1
  67. package/src/types.ts +38 -3
  68. package/src/utils/ensure-function-metadata.ts +24 -0
  69. package/src/utils/extract-function-name.ts +20 -8
  70. package/src/utils/filter-inspector-state.test.ts +1433 -0
  71. package/src/utils/filter-inspector-state.ts +526 -0
  72. package/src/utils/filter-utils.test.ts +350 -1
  73. package/src/utils/filter-utils.ts +82 -2
  74. package/src/utils/find-root-dir.ts +68 -0
  75. package/src/utils/get-files-and-methods.ts +10 -2
  76. package/src/utils/get-property-value.ts +27 -0
  77. package/src/utils/middleware.ts +14 -7
  78. package/src/utils/permissions.test.ts +327 -0
  79. package/src/utils/permissions.ts +262 -0
  80. package/src/utils/post-process.ts +178 -0
  81. package/src/utils/serialize-inspector-state.ts +375 -0
  82. package/src/utils/test-data/inspector-state.json +1680 -0
  83. package/src/visit.ts +4 -2
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -5,11 +5,15 @@ import {
5
5
  InspectorOptions,
6
6
  InspectorState,
7
7
  } from '../types.js'
8
- import { CLIProgramMeta, CLICommandMeta } from '@pikku/core'
8
+ import { CLIProgramMeta, CLICommandMeta } from '@pikku/core/cli'
9
9
  import { extractFunctionName } from '../utils/extract-function-name.js'
10
10
  import { resolveMiddleware } from '../utils/middleware.js'
11
+ import { extractWireNames } from '../utils/post-process.js'
11
12
  import { getPropertyValue } from '../utils/get-property-value.js'
12
13
 
14
+ // Track if we've warned about missing Config type to avoid duplicate warnings
15
+ const configTypeWarningShown = new Set<string>()
16
+
13
17
  /**
14
18
  * Adds CLI command metadata to the inspector state
15
19
  */
@@ -60,7 +64,8 @@ export const addCLI: AddWiring = (
60
64
  }
61
65
 
62
66
  // Add this program to the CLI metadata
63
- inspectorState.cli.meta[cliConfig.programName] = cliConfig.programMeta
67
+ inspectorState.cli.meta.programs[cliConfig.programName] =
68
+ cliConfig.programMeta
64
69
  }
65
70
 
66
71
  /**
@@ -75,12 +80,33 @@ function processCLIConfig(
75
80
  options: InspectorOptions
76
81
  ): { programName: string; programMeta: CLIProgramMeta } | null {
77
82
  let programName = ''
83
+ let programTags: string[] | undefined
78
84
  const programMeta: CLIProgramMeta = {
79
85
  program: '',
80
86
  commands: {},
81
87
  options: {},
82
88
  }
83
89
 
90
+ // First pass: extract program name and tags
91
+ for (const prop of node.properties) {
92
+ if (!ts.isPropertyAssignment(prop)) continue
93
+ if (!ts.isIdentifier(prop.name)) continue
94
+
95
+ const propName = prop.name.text
96
+
97
+ if (propName === 'program' && ts.isStringLiteral(prop.initializer)) {
98
+ programName = prop.initializer.text
99
+ programMeta.program = programName
100
+ } else if (propName === 'tags') {
101
+ programTags = (getPropertyValue(node, 'tags') as string[]) || undefined
102
+ }
103
+ }
104
+
105
+ if (!programName) {
106
+ return null
107
+ }
108
+
109
+ // Second pass: process other properties with program tags available
84
110
  for (const prop of node.properties) {
85
111
  if (!ts.isPropertyAssignment(prop)) continue
86
112
  if (!ts.isIdentifier(prop.name)) continue
@@ -89,10 +115,8 @@ function processCLIConfig(
89
115
 
90
116
  switch (propName) {
91
117
  case 'program':
92
- if (ts.isStringLiteral(prop.initializer)) {
93
- programName = prop.initializer.text
94
- programMeta.program = programName
95
- }
118
+ case 'tags':
119
+ // Already handled in first pass
96
120
  break
97
121
 
98
122
  case 'commands':
@@ -104,7 +128,8 @@ function processCLIConfig(
104
128
  typeChecker,
105
129
  programName,
106
130
  inspectorState,
107
- options
131
+ options,
132
+ programTags
108
133
  )
109
134
  }
110
135
  break
@@ -122,16 +147,16 @@ function processCLIConfig(
122
147
  break
123
148
 
124
149
  case 'render':
125
- // Track that a default renderer exists
126
- programMeta.defaultRenderName = 'defaultRenderer'
150
+ // Extract the actual renderer function name
151
+ programMeta.defaultRenderName = extractFunctionName(
152
+ prop.initializer,
153
+ typeChecker,
154
+ inspectorState.rootDir
155
+ ).pikkuFuncName
127
156
  break
128
157
  }
129
158
  }
130
159
 
131
- if (!programName) {
132
- return null
133
- }
134
-
135
160
  return { programName, programMeta }
136
161
  }
137
162
 
@@ -145,9 +170,11 @@ function processCommands(
145
170
  typeChecker: TypeChecker,
146
171
  programName: string,
147
172
  inspectorState: InspectorState,
148
- options: InspectorOptions
173
+ options: InspectorOptions,
174
+ programTags?: string[]
149
175
  ): Record<string, CLICommandMeta> {
150
176
  const commands: Record<string, CLICommandMeta> = {}
177
+ let defaultCommandName: string | null = null
151
178
 
152
179
  for (const prop of node.properties) {
153
180
  if (!ts.isPropertyAssignment(prop)) continue
@@ -163,11 +190,29 @@ function processCommands(
163
190
  prop.initializer,
164
191
  sourceFile,
165
192
  typeChecker,
166
- programName
193
+ programName,
194
+ [],
195
+ programTags
167
196
  )
168
197
 
169
198
  if (commandMeta) {
170
199
  commands[commandName] = commandMeta
200
+
201
+ // Validate only one default command
202
+ if (commandMeta.isDefault) {
203
+ if (defaultCommandName !== null) {
204
+ const position = prop.getStart(sourceFile)
205
+ const { line, character } =
206
+ sourceFile.getLineAndCharacterOfPosition(position)
207
+
208
+ throw new Error(
209
+ `Multiple default commands found in CLI program "${programName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
210
+ `Commands "${defaultCommandName}" and "${commandName}" are both marked as default.\n` +
211
+ `Only one command can be marked as default per program.`
212
+ )
213
+ }
214
+ defaultCommandName = commandName
215
+ }
171
216
  }
172
217
  }
173
218
 
@@ -186,7 +231,8 @@ function processCommand(
186
231
  sourceFile: ts.SourceFile,
187
232
  typeChecker: TypeChecker,
188
233
  programName: string,
189
- parentPath: string[] = []
234
+ parentPath: string[] = [],
235
+ programTags?: string[]
190
236
  ): CLICommandMeta | null {
191
237
  const fullPath = [...parentPath, name]
192
238
 
@@ -197,7 +243,11 @@ function processCommand(
197
243
  ts.isFunctionExpression(node)
198
244
  ) {
199
245
  return {
200
- pikkuFuncName: extractFunctionName(node, typeChecker).pikkuFuncName,
246
+ pikkuFuncName: extractFunctionName(
247
+ node,
248
+ typeChecker,
249
+ inspectorState.rootDir
250
+ ).pikkuFuncName,
201
251
  positionals: [],
202
252
  options: {},
203
253
  }
@@ -222,7 +272,8 @@ function processCommand(
222
272
  sourceFile,
223
273
  typeChecker,
224
274
  programName,
225
- parentPath
275
+ parentPath,
276
+ programTags
226
277
  )
227
278
  }
228
279
  return null
@@ -253,7 +304,8 @@ function processCommand(
253
304
  if (propName === 'func') {
254
305
  pikkuFuncName = extractFunctionName(
255
306
  prop.initializer,
256
- typeChecker
307
+ typeChecker,
308
+ inspectorState.rootDir
257
309
  ).pikkuFuncName
258
310
  meta.pikkuFuncName = pikkuFuncName
259
311
  } else if (
@@ -266,12 +318,25 @@ function processCommand(
266
318
  }
267
319
  }
268
320
 
321
+ // Merge program-level tags with command-level tags
322
+ const allTags = [...(programTags || []), ...(tags || [])]
323
+
269
324
  // Resolve middleware
270
- const middleware = resolveMiddleware(inspectorState, node, tags, typeChecker)
325
+ const middleware = resolveMiddleware(
326
+ inspectorState,
327
+ node,
328
+ allTags.length > 0 ? allTags : undefined,
329
+ typeChecker
330
+ )
271
331
  if (middleware) {
272
332
  meta.middleware = middleware
273
333
  }
274
334
 
335
+ // Add merged tags to metadata
336
+ if (allTags.length > 0) {
337
+ meta.tags = allTags
338
+ }
339
+
275
340
  // Second pass: process all properties
276
341
  for (const prop of node.properties) {
277
342
  if (!ts.isPropertyAssignment(prop)) continue
@@ -300,7 +365,8 @@ function processCommand(
300
365
  case 'render':
301
366
  meta.renderName = extractFunctionName(
302
367
  prop.initializer,
303
- typeChecker
368
+ typeChecker,
369
+ inspectorState.rootDir
304
370
  ).pikkuFuncName
305
371
  break
306
372
 
@@ -336,7 +402,8 @@ function processCommand(
336
402
  sourceFile,
337
403
  typeChecker,
338
404
  programName,
339
- fullPath
405
+ fullPath,
406
+ programTags
340
407
  )
341
408
 
342
409
  if (subCommand) {
@@ -345,9 +412,25 @@ function processCommand(
345
412
  }
346
413
  }
347
414
  break
415
+
416
+ case 'isDefault':
417
+ if (
418
+ prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
419
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword
420
+ ) {
421
+ meta.isDefault = prop.initializer.kind === ts.SyntaxKind.TrueKeyword
422
+ }
423
+ break
348
424
  }
349
425
  }
350
426
 
427
+ // --- track used functions/middleware for service aggregation ---
428
+ inspectorState.serviceAggregation.usedFunctions.add(meta.pikkuFuncName)
429
+ extractWireNames(meta.middleware).forEach((name) =>
430
+ inspectorState.serviceAggregation.usedMiddleware.add(name)
431
+ )
432
+ // Note: subcommands are tracked recursively when they're processed
433
+
351
434
  return meta
352
435
  }
353
436
 
@@ -575,19 +658,24 @@ function extractEnumFromConfigType(
575
658
  // Look for Config type in typesLookup
576
659
  const configTypes = inspectorState.typesLookup.get('Config')
577
660
  if (!configTypes || configTypes.length === 0) {
578
- logger.warn(
579
- `Warning: Could not find Config type in typesLookup for option "${propertyName}". ` +
580
- `Make sure you have a Config interface extending CoreConfig in your codebase.`
581
- )
661
+ // Only warn once per CLI file to avoid spamming logs
662
+ if (!configTypeWarningShown.has('missing-config-type')) {
663
+ configTypeWarningShown.add('missing-config-type')
664
+ logger.warn(
665
+ `Could not find Config type in typesLookup. ` +
666
+ `Make sure you have a Config interface extending CoreConfig in your codebase.`
667
+ )
668
+ }
582
669
  return null
583
670
  }
584
671
 
585
672
  // Use the first Config type (there should only be one)
586
673
  const configType = configTypes[0]
587
674
  if (!configType) {
588
- logger.warn(
589
- `Warning: Config type is undefined in typesLookup for option "${propertyName}".`
590
- )
675
+ if (!configTypeWarningShown.has('undefined-config-type')) {
676
+ configTypeWarningShown.add('undefined-config-type')
677
+ logger.warn(`Config type is undefined in typesLookup.`)
678
+ }
591
679
  return null
592
680
  }
593
681
 
@@ -661,3 +749,74 @@ function parseCommandPattern(pattern: string): any[] {
661
749
 
662
750
  return positionals
663
751
  }
752
+
753
+ /**
754
+ * Adds CLI renderer metadata to the inspector state
755
+ */
756
+ export const addCLIRenderers: AddWiring = (
757
+ logger,
758
+ node,
759
+ typeChecker,
760
+ inspectorState,
761
+ options
762
+ ) => {
763
+ if (!ts.isCallExpression(node)) return
764
+
765
+ const { expression, arguments: args, typeArguments } = node
766
+
767
+ // Only handle pikkuCLIRender calls
768
+ if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
769
+ return
770
+ }
771
+
772
+ if (args.length === 0) return
773
+
774
+ // Extract renderer name
775
+ const { pikkuFuncName, exportedName } = extractFunctionName(
776
+ node,
777
+ typeChecker,
778
+ inspectorState.rootDir
779
+ )
780
+
781
+ // Get the source file path
782
+ const sourceFile = node.getSourceFile()
783
+ const filePath = sourceFile.fileName
784
+
785
+ // Extract services from type parameters (second type param is Services)
786
+ const services: { optimized: boolean; services: string[] } = {
787
+ optimized: true,
788
+ services: [],
789
+ }
790
+
791
+ if (typeArguments && typeArguments.length >= 2) {
792
+ // Second type parameter is the Services type
793
+ const servicesTypeNode = typeArguments[1]
794
+ if (servicesTypeNode) {
795
+ const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode)
796
+
797
+ // Extract property names from the Services type
798
+ const properties = servicesType.getProperties()
799
+ for (const prop of properties) {
800
+ services.services.push(prop.getName())
801
+ }
802
+
803
+ // If no specific services found, it might be using the full services object
804
+ if (properties.length === 0) {
805
+ services.optimized = false
806
+ }
807
+ }
808
+ }
809
+
810
+ // Store renderer metadata
811
+ inspectorState.cli.meta.renderers[pikkuFuncName] = {
812
+ name: pikkuFuncName,
813
+ exportedName: exportedName ?? undefined,
814
+ services,
815
+ filePath,
816
+ }
817
+
818
+ // Add to files map if exported
819
+ if (exportedName) {
820
+ inspectorState.cli.files.add(filePath)
821
+ }
822
+ }
@@ -36,11 +36,14 @@ export const addFileExtendsCoreType = (
36
36
  const variables = methods.get(fileName) || []
37
37
 
38
38
  if (!typeName) {
39
- throw new Error('TODO')
39
+ throw new Error(
40
+ `Found anonymous ${ts.isClassDeclaration(node) ? 'class' : 'interface'} extending ${expectedTypeName} in ${fileName}. ` +
41
+ `Classes and interfaces that extend core types must have a name.`
42
+ )
40
43
  }
41
44
  variables.push({
42
45
  variable: typeName,
43
- type: typeName || null,
46
+ type: typeName,
44
47
  typePath: extendedTypeDeclarationPath,
45
48
  })
46
49
  methods.set(fileName, variables)
@@ -1,17 +1,88 @@
1
1
  import * as ts from 'typescript'
2
- import { PathToNameAndType } from '../types.js'
2
+ import { PathToNameAndType, InspectorState } from '../types.js'
3
+ import { extractServicesFromFunction } from '../utils/extract-services.js'
4
+
5
+ // Mapping of wrapper function names to their corresponding types
6
+ const wrapperFunctionMap: Record<string, string> = {
7
+ pikkuConfig: 'CreateConfig',
8
+ pikkuServices: 'CreateSingletonServices',
9
+ pikkuSessionServices: 'CreateSessionServices',
10
+ }
3
11
 
4
12
  export const addFileWithFactory = (
5
13
  node: ts.Node,
6
14
  checker: ts.TypeChecker,
7
15
  methods: PathToNameAndType = new Map(),
8
- expectedTypeName: string
16
+ expectedTypeName: string,
17
+ state?: InspectorState
9
18
  ) => {
10
19
  if (ts.isVariableDeclaration(node)) {
11
20
  const fileName = node.getSourceFile().fileName
12
21
  const variableTypeNode = node.type
13
22
  const variableName = node.name.getText()
14
23
 
24
+ // Check for wrapper function calls FIRST (e.g., pikkuConfig(...), pikkuServices(...))
25
+ // This handles both cases: with and without explicit type annotations
26
+ if (node.initializer && ts.isCallExpression(node.initializer)) {
27
+ const callExpression = node.initializer
28
+ const expression = callExpression.expression
29
+
30
+ if (ts.isIdentifier(expression)) {
31
+ const wrapperFunctionName = expression.text
32
+ const inferredType = wrapperFunctionMap[wrapperFunctionName]
33
+
34
+ if (inferredType === expectedTypeName) {
35
+ // Get the type declaration path from the wrapper function
36
+ const typeSymbol = checker.getSymbolAtLocation(expression)
37
+ let typeDeclarationPath: string | null = null
38
+
39
+ if (
40
+ typeSymbol &&
41
+ typeSymbol.declarations &&
42
+ typeSymbol.declarations[0]
43
+ ) {
44
+ const declaration = typeSymbol.declarations[0]
45
+ const sourceFile = declaration.getSourceFile()
46
+ typeDeclarationPath = sourceFile.fileName
47
+ }
48
+
49
+ const variables = methods.get(fileName) || []
50
+ variables.push({
51
+ variable: variableName,
52
+ type: inferredType,
53
+ typePath: typeDeclarationPath,
54
+ })
55
+ methods.set(fileName, variables)
56
+
57
+ // Extract singleton services for CreateSessionServices factories
58
+ if (
59
+ expectedTypeName === 'CreateSessionServices' &&
60
+ state &&
61
+ callExpression.arguments.length > 0
62
+ ) {
63
+ const firstArg = callExpression.arguments[0]
64
+ let functionNode:
65
+ | ts.ArrowFunction
66
+ | ts.FunctionExpression
67
+ | undefined
68
+
69
+ if (ts.isArrowFunction(firstArg)) {
70
+ functionNode = firstArg
71
+ } else if (ts.isFunctionExpression(firstArg)) {
72
+ functionNode = firstArg
73
+ }
74
+
75
+ if (functionNode) {
76
+ const servicesMeta = extractServicesFromFunction(functionNode)
77
+ state.sessionServicesMeta.set(variableName, servicesMeta.services)
78
+ }
79
+ }
80
+
81
+ return // Early return since we found a match
82
+ }
83
+ }
84
+ }
85
+
15
86
  if (variableTypeNode && ts.isTypeReferenceNode(variableTypeNode)) {
16
87
  const typeNameNode = variableTypeNode.typeName || null
17
88
 
@@ -37,6 +108,25 @@ export const addFileWithFactory = (
37
108
  typePath: typeDeclarationPath,
38
109
  })
39
110
  methods.set(fileName, variables)
111
+
112
+ // Extract singleton services for CreateSessionServices factories
113
+ if (
114
+ expectedTypeName === 'CreateSessionServices' &&
115
+ state &&
116
+ node.initializer
117
+ ) {
118
+ let functionNode: ts.ArrowFunction | ts.FunctionExpression | undefined
119
+ if (ts.isArrowFunction(node.initializer)) {
120
+ functionNode = node.initializer
121
+ } else if (ts.isFunctionExpression(node.initializer)) {
122
+ functionNode = node.initializer
123
+ }
124
+
125
+ if (functionNode) {
126
+ const servicesMeta = extractServicesFromFunction(functionNode)
127
+ state.sessionServicesMeta.set(variableName, servicesMeta.services)
128
+ }
129
+ }
40
130
  }
41
131
 
42
132
  // Handle qualified type names if necessary
@@ -58,6 +148,28 @@ export const addFileWithFactory = (
58
148
  typePath: typeDeclarationPath,
59
149
  })
60
150
  methods.set(fileName, variables)
151
+
152
+ // Extract singleton services for CreateSessionServices factories
153
+ if (
154
+ expectedTypeName === 'CreateSessionServices' &&
155
+ state &&
156
+ node.initializer
157
+ ) {
158
+ let functionNode:
159
+ | ts.ArrowFunction
160
+ | ts.FunctionExpression
161
+ | undefined
162
+ if (ts.isArrowFunction(node.initializer)) {
163
+ functionNode = node.initializer
164
+ } else if (ts.isFunctionExpression(node.initializer)) {
165
+ functionNode = node.initializer
166
+ }
167
+
168
+ if (functionNode) {
169
+ const servicesMeta = extractServicesFromFunction(functionNode)
170
+ state.sessionServicesMeta.set(variableName, servicesMeta.services)
171
+ }
172
+ }
61
173
  }
62
174
  }
63
175
  }
@@ -27,7 +27,8 @@ const nullifyTypes = (type: string | null) => {
27
27
  const resolveTypeImports = (
28
28
  type: ts.Type,
29
29
  resolvedTypes: TypesMap,
30
- isCustom: boolean
30
+ isCustom: boolean,
31
+ checker: ts.TypeChecker
31
32
  ): string[] => {
32
33
  const types: string[] = []
33
34
 
@@ -42,10 +43,14 @@ const resolveTypeImports = (
42
43
  const path = sourceFile.fileName
43
44
 
44
45
  // Skip built-in utility types or TypeScript lib types
46
+ // Skip enum members (but not the enum type itself)
47
+ const isEnumMember = declaration && ts.isEnumMember(declaration)
48
+
45
49
  if (
46
50
  !path.includes('node_modules/typescript') &&
47
51
  symbol.getName() !== '__type' &&
48
- !isPrimitiveType(currentType)
52
+ !isPrimitiveType(currentType) &&
53
+ !isEnumMember
49
54
  ) {
50
55
  const originalName = symbol.getName()
51
56
  // Check if the type is already in the map
@@ -102,6 +107,32 @@ const resolveTypeImports = (
102
107
  const typeRef = currentType as ts.TypeReference
103
108
  typeRef.typeArguments?.forEach(visitType)
104
109
  }
110
+
111
+ // Handle anonymous object types with enum properties (e.g., { userType: UserType })
112
+ // Only traverse into enum property types to avoid over-importing other named types
113
+ if (currentType.flags & ts.TypeFlags.Object) {
114
+ const objectType = currentType as ts.ObjectType
115
+ const typeSymbol = objectType.getSymbol()
116
+
117
+ // Only traverse properties for anonymous object types (no symbol or __type symbol)
118
+ // Skip named types, interfaces, and enums to avoid over-importing
119
+ const isAnonymousObject = !typeSymbol || typeSymbol.getName() === '__type'
120
+
121
+ if (isAnonymousObject) {
122
+ const properties = objectType.getProperties()
123
+ for (const prop of properties) {
124
+ if (prop.valueDeclaration) {
125
+ const propType = checker.getTypeOfSymbolAtLocation(
126
+ prop,
127
+ prop.valueDeclaration
128
+ )
129
+ if (propType) {
130
+ visitType(propType)
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
105
136
  }
106
137
 
107
138
  visitType(type)
@@ -170,7 +201,7 @@ const getNamesAndTypes = (
170
201
 
171
202
  // record the alias in your TypesMap
172
203
  const references = rawTypes
173
- .map((t) => resolveTypeImports(t, typesMap, true))
204
+ .map((t) => resolveTypeImports(t, typesMap, true, checker))
174
205
  .flat()
175
206
 
176
207
  typesMap.addCustomType(aliasName, aliasType, references)
@@ -192,7 +223,7 @@ const getNamesAndTypes = (
192
223
  return name
193
224
  }
194
225
  // non-primitive: import/alias it inline
195
- return resolveTypeImports(t, typesMap, false)
226
+ return resolveTypeImports(t, typesMap, false, checker)
196
227
  })
197
228
  .flat()
198
229
 
@@ -270,7 +301,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
270
301
  if (args.length === 0) return
271
302
 
272
303
  const { pikkuFuncName, name, explicitName, exportedName } =
273
- extractFunctionName(node, checker)
304
+ extractFunctionName(node, checker, state.rootDir)
274
305
 
275
306
  let tags: string[] | undefined
276
307
  let expose: boolean | undefined
@@ -300,6 +331,17 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
300
331
  (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))
301
332
  ) {
302
333
  logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
334
+ // Create stub metadata to prevent "function not found" errors in wirings
335
+ state.functions.meta[pikkuFuncName] = {
336
+ pikkuFuncName,
337
+ name,
338
+ services: { optimized: false, services: [] },
339
+ inputSchemaName: null,
340
+ outputSchemaName: null,
341
+ inputs: [],
342
+ outputs: [],
343
+ middleware: undefined,
344
+ }
303
345
  return
304
346
  }
305
347
  handlerNode = fnProp
@@ -310,6 +352,17 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
310
352
  !ts.isFunctionExpression(handlerNode)
311
353
  ) {
312
354
  logger.error(`• Handler for ${name} is not a function.`)
355
+ // Create stub metadata to prevent "function not found" errors in wirings
356
+ state.functions.meta[pikkuFuncName] = {
357
+ pikkuFuncName,
358
+ name,
359
+ services: { optimized: false, services: [] },
360
+ inputSchemaName: null,
361
+ outputSchemaName: null,
362
+ inputs: [],
363
+ outputs: [],
364
+ middleware: undefined,
365
+ }
313
366
  return
314
367
  }
315
368
 
@@ -434,6 +487,8 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
434
487
  path: node.getSourceFile().fileName,
435
488
  exportedName,
436
489
  })
490
+ // Track exposed RPC function for service aggregation
491
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName)
437
492
  }
438
493
 
439
494
  // We add it to internal meta to allow autocomplete for everything