@pikku/inspector 0.9.2 → 0.9.4

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.
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import { InspectorState, InspectorFilters, InspectorLogger } from './types.js'
2
+ import { InspectorLogger, InspectorState } from './types.js'
3
3
  import { TypesMap } from './types-map.js'
4
4
  import {
5
5
  extractFunctionName,
@@ -84,6 +84,25 @@ const resolveTypeImports = (
84
84
  typeRef.typeArguments?.forEach(visitType)
85
85
  }
86
86
  }
87
+
88
+ // Always traverse type arguments for thorough type collection
89
+ if (currentType.aliasTypeArguments) {
90
+ currentType.aliasTypeArguments.forEach(visitType)
91
+ }
92
+
93
+ // Always handle intersections and unions
94
+ if (currentType.isUnionOrIntersection()) {
95
+ currentType.types.forEach(visitType)
96
+ }
97
+
98
+ // Always handle object types with type arguments
99
+ if (
100
+ currentType.flags & ts.TypeFlags.Object &&
101
+ (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
102
+ ) {
103
+ const typeRef = currentType as ts.TypeReference
104
+ typeRef.typeArguments?.forEach(visitType)
105
+ }
87
106
  }
88
107
 
89
108
  visitType(type)
@@ -232,7 +251,6 @@ export function addFunctions(
232
251
  node: ts.Node,
233
252
  checker: ts.TypeChecker,
234
253
  state: InspectorState,
235
- filters: InspectorFilters,
236
254
  logger: InspectorLogger
237
255
  ) {
238
256
  if (!ts.isCallExpression(node)) return
@@ -267,7 +285,10 @@ export function addFunctions(
267
285
  // determine the actual handler expression:
268
286
  // either the `func` prop or the first argument directly
269
287
  let handlerNode: ts.Expression = args[0]!
288
+ let isDirectFunction = true // Default to direct function format
289
+
270
290
  if (ts.isObjectLiteralExpression(handlerNode)) {
291
+ isDirectFunction = false // This is object format with func property
271
292
  tags = (getPropertyValue(handlerNode, 'tags') as string[]) || undefined
272
293
  expose = getPropertyValue(handlerNode, 'expose') as boolean | undefined
273
294
  docs = getPropertyValue(handlerNode, 'docs') as PikkuDocs | undefined
@@ -282,7 +303,7 @@ export function addFunctions(
282
303
  !fnProp ||
283
304
  (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))
284
305
  ) {
285
- console.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
306
+ logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
286
307
  return
287
308
  }
288
309
  handlerNode = fnProp
@@ -292,7 +313,7 @@ export function addFunctions(
292
313
  !ts.isArrowFunction(handlerNode) &&
293
314
  !ts.isFunctionExpression(handlerNode)
294
315
  ) {
295
- console.error(`• Handler for ${name} is not a function.`)
316
+ logger.error(`• Handler for ${name} is not a function.`)
296
317
  return
297
318
  }
298
319
 
@@ -334,7 +355,7 @@ export function addFunctions(
334
355
  genericTypes[0]
335
356
  )
336
357
  // if (inputTypes.length === 0) {
337
- // console.debug(
358
+ // logger.debug(
338
359
  // `\x1b[31m• Unknown input type for '${name}', assuming void.\x1b[0m`
339
360
  // )
340
361
  // }
@@ -365,7 +386,7 @@ export function addFunctions(
365
386
  }
366
387
 
367
388
  if (inputNames.length > 1) {
368
- console.warn(
389
+ logger.warn(
369
390
  'More than one input type detected, only the first one will be used as a schema.'
370
391
  )
371
392
  }
@@ -374,34 +395,42 @@ export function addFunctions(
374
395
  pikkuFuncName,
375
396
  name,
376
397
  services,
377
- schemaName: inputNames[0] ?? null,
398
+ inputSchemaName: inputNames[0] ?? null,
399
+ outputSchemaName: outputNames[0] ?? null,
378
400
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
379
401
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
380
- expose,
381
- tags,
382
- docs,
402
+ expose: expose || undefined,
403
+ tags: tags || undefined,
404
+ docs: docs || undefined,
405
+ isDirectFunction,
383
406
  }
384
407
 
385
- if (explicitName || exportedName) {
408
+ if (exportedName || explicitName) {
386
409
  if (!exportedName) {
387
- console.error(
410
+ logger.error(
388
411
  `• Function with explicit name '${name}' is not exported, this is not allowed.`
389
412
  )
390
413
  return
391
414
  }
392
- if (state.rpc.meta[name]) {
393
- console.error(`• Function name '${name}' already exists, skipping.`)
394
- return
415
+
416
+ if (expose) {
417
+ state.rpc.exposedMeta[name] = pikkuFuncName
418
+ state.rpc.exposedFiles.set(name, {
419
+ path: node.getSourceFile().fileName,
420
+ exportedName,
421
+ })
395
422
  }
396
- state.rpc.meta[name] = {
397
- pikkuFuncName,
398
- exposed: false,
423
+
424
+ // We add it to internal meta to allow autocomplete for everything
425
+ state.rpc.internalMeta[name] = pikkuFuncName
426
+
427
+ // But we only import the actual function if it's actually invoked to keep
428
+ // bundle size down
429
+ if (state.rpc.invokedFunctions.has(pikkuFuncName)) {
430
+ state.rpc.internalFiles.set(name, {
431
+ path: node.getSourceFile().fileName,
432
+ exportedName,
433
+ })
399
434
  }
400
- state.functions.files.set(name, {
401
- path: node.getSourceFile().fileName,
402
- exportedName,
403
- })
404
- } else {
405
- console.log(`• Function name '${name}' not exported, skipping.`)
406
435
  }
407
436
  }
@@ -116,7 +116,7 @@ export const addHTTPRoute = (
116
116
 
117
117
  // --- record route ---
118
118
  state.http.files.add(node.getSourceFile().fileName)
119
- state.http.meta.push({
119
+ state.http.meta[method][route] = {
120
120
  pikkuFuncName: funcName,
121
121
  route,
122
122
  method: method as HTTPMethod,
@@ -125,5 +125,5 @@ export const addHTTPRoute = (
125
125
  inputTypes,
126
126
  docs,
127
127
  tags,
128
- })
128
+ }
129
129
  }
@@ -0,0 +1,51 @@
1
+ import * as ts from 'typescript'
2
+ import { InspectorLogger, InspectorState } from './types.js'
3
+ import { extractFunctionName, extractServicesFromFunction } from './utils.js'
4
+
5
+ /**
6
+ * Inspect pikkuMiddleware calls and extract first-arg destructuring
7
+ * for tree shaking optimization.
8
+ */
9
+ export function addMiddleware(
10
+ node: ts.Node,
11
+ checker: ts.TypeChecker,
12
+ state: InspectorState,
13
+ logger: InspectorLogger
14
+ ) {
15
+ if (!ts.isCallExpression(node)) return
16
+
17
+ const { expression, arguments: args } = node
18
+
19
+ // only handle calls like pikkuMiddleware(...)
20
+ if (!ts.isIdentifier(expression)) {
21
+ return
22
+ }
23
+
24
+ if (expression.text !== 'pikkuMiddleware') {
25
+ return
26
+ }
27
+
28
+ const handlerNode = args[0]
29
+ if (!handlerNode) return
30
+
31
+ if (
32
+ !ts.isArrowFunction(handlerNode) &&
33
+ !ts.isFunctionExpression(handlerNode)
34
+ ) {
35
+ logger.error(`• Handler for pikkuMiddleware is not a function.`)
36
+ return
37
+ }
38
+
39
+ const services = extractServicesFromFunction(handlerNode)
40
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker)
41
+ state.middleware.meta[pikkuFuncName] = {
42
+ services,
43
+ sourceFile: node.getSourceFile().fileName,
44
+ position: node.getStart(),
45
+ exportedName,
46
+ }
47
+
48
+ logger.debug(
49
+ `• Found middleware with services: ${services.services.join(', ')}`
50
+ )
51
+ }
@@ -0,0 +1,53 @@
1
+ import * as ts from 'typescript'
2
+ import { InspectorLogger, InspectorState } from './types.js'
3
+ import { extractFunctionName, extractServicesFromFunction } from './utils.js'
4
+
5
+ /**
6
+ * Inspect pikkuPermission calls and extract first-arg destructuring
7
+ * for tree shaking optimization.
8
+ */
9
+ export function addPermission(
10
+ node: ts.Node,
11
+ checker: ts.TypeChecker,
12
+ state: InspectorState,
13
+ logger: InspectorLogger
14
+ ) {
15
+ if (!ts.isCallExpression(node)) return
16
+
17
+ const { expression, arguments: args } = node
18
+
19
+ // only handle calls like pikkuPermission(...)
20
+ if (!ts.isIdentifier(expression)) {
21
+ return
22
+ }
23
+
24
+ if (expression.text !== 'pikkuPermission') {
25
+ return
26
+ }
27
+
28
+ const handlerNode = args[0]
29
+ if (!handlerNode) return
30
+
31
+ if (
32
+ !ts.isArrowFunction(handlerNode) &&
33
+ !ts.isFunctionExpression(handlerNode)
34
+ ) {
35
+ logger.error(`• Handler for pikkuPermission is not a function.`)
36
+ return
37
+ }
38
+
39
+ const services = extractServicesFromFunction(handlerNode)
40
+
41
+ const { pikkuFuncName, exportedName } = extractFunctionName(node, checker)
42
+
43
+ state.permissions.meta[pikkuFuncName] = {
44
+ services,
45
+ sourceFile: node.getSourceFile().fileName,
46
+ position: node.getStart(),
47
+ exportedName,
48
+ }
49
+
50
+ logger.debug(
51
+ `• Found permission with services: ${services.services.join(', ')}`
52
+ )
53
+ }
@@ -0,0 +1,49 @@
1
+ import * as ts from 'typescript'
2
+ import { InspectorState, InspectorLogger } from './types.js'
3
+
4
+ /**
5
+ * Scan for rpc.invoke() calls to track which functions are actually being invoked
6
+ */
7
+ export function addRPCInvocations(
8
+ node: ts.Node,
9
+ state: InspectorState,
10
+ logger: InspectorLogger
11
+ ) {
12
+ // Look for property access expressions: rpc.invoke
13
+ if (ts.isPropertyAccessExpression(node)) {
14
+ const { expression, name } = node
15
+
16
+ // Check if this is accessing 'invoke' property
17
+ if (name.text === 'invoke') {
18
+ // Check if the object is 'rpc' (or a variable containing rpc)
19
+ if (ts.isIdentifier(expression) && expression.text === 'rpc') {
20
+ // This is rpc.invoke - now we need to find the parent call expression
21
+ const parent = node.parent
22
+ if (ts.isCallExpression(parent) && parent.expression === node) {
23
+ // This is rpc.invoke('function-name')
24
+ const [firstArg] = parent.arguments
25
+ if (firstArg) {
26
+ // Extract the function name from string literal
27
+ if (ts.isStringLiteral(firstArg)) {
28
+ const functionName = firstArg.text
29
+ logger.debug(`• Found RPC invocation: ${functionName}`)
30
+ state.rpc.invokedFunctions.add(functionName)
31
+ }
32
+ // Handle template literals like `function-${name}`
33
+ else if (
34
+ ts.isTemplateExpression(firstArg) ||
35
+ ts.isNoSubstitutionTemplateLiteral(firstArg)
36
+ ) {
37
+ logger.warn(
38
+ `• Found dynamic RPC invocation: ${firstArg.getText()}`
39
+ )
40
+ logger.warn(
41
+ `\tYou can only use string literals for RPC function names, with ' or " and not \``
42
+ )
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
package/src/inspector.ts CHANGED
@@ -36,11 +36,18 @@ export const inspect = (
36
36
  functions: {
37
37
  typesMap: new TypesMap(),
38
38
  meta: {},
39
- files: new Map(),
40
39
  },
41
40
  http: {
42
41
  metaInputTypes: new Map(),
43
- meta: [],
42
+ meta: {
43
+ get: {},
44
+ post: {},
45
+ put: {},
46
+ delete: {},
47
+ head: {},
48
+ patch: {},
49
+ options: {},
50
+ },
44
51
  files: new Set(),
45
52
  },
46
53
  channels: {
@@ -56,7 +63,11 @@ export const inspect = (
56
63
  files: new Set(),
57
64
  },
58
65
  rpc: {
59
- meta: {},
66
+ internalMeta: {},
67
+ internalFiles: new Map(),
68
+ exposedMeta: {},
69
+ exposedFiles: new Map(),
70
+ invokedFunctions: new Set(),
60
71
  },
61
72
  mcpEndpoints: {
62
73
  resourcesMeta: {},
@@ -64,6 +75,12 @@ export const inspect = (
64
75
  promptsMeta: {},
65
76
  files: new Set(),
66
77
  },
78
+ middleware: {
79
+ meta: {},
80
+ },
81
+ permissions: {
82
+ meta: {},
83
+ },
67
84
  }
68
85
 
69
86
  // First sweep: add all functions
package/src/types.ts CHANGED
@@ -4,8 +4,7 @@ import { ScheduledTasksMeta } from '@pikku/core/scheduler'
4
4
  import { queueWorkersMeta } from '@pikku/core/queue'
5
5
  import { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core'
6
6
  import { TypesMap } from './types-map.js'
7
- import { FunctionsMeta } from '@pikku/core'
8
- import { RPCMeta } from '@pikku/core/rpc'
7
+ import { FunctionsMeta, FunctionServicesMeta } from '@pikku/core'
9
8
 
10
9
  export type PathToNameAndType = Map<
11
10
  string,
@@ -29,7 +28,6 @@ export interface InspectorHTTPState {
29
28
 
30
29
  export interface InspectorFunctionState {
31
30
  typesMap: TypesMap
32
- files: Map<string, { path: string; exportedName: string }>
33
31
  meta: FunctionsMeta
34
32
  }
35
33
 
@@ -38,6 +36,30 @@ export interface InspectorChannelState {
38
36
  files: Set<string>
39
37
  }
40
38
 
39
+ export interface InspectorMiddlewareState {
40
+ meta: Record<
41
+ string,
42
+ {
43
+ services: FunctionServicesMeta
44
+ sourceFile: string
45
+ position: number
46
+ exportedName: string | null
47
+ }
48
+ >
49
+ }
50
+
51
+ export interface InspectorPermissionState {
52
+ meta: Record<
53
+ string,
54
+ {
55
+ services: FunctionServicesMeta
56
+ sourceFile: string
57
+ position: number
58
+ exportedName: string | null
59
+ }
60
+ >
61
+ }
62
+
41
63
  export type InspectorFilters = {
42
64
  tags?: string[]
43
65
  types?: string[]
@@ -69,7 +91,11 @@ export interface InspectorState {
69
91
  files: Set<string>
70
92
  }
71
93
  rpc: {
72
- meta: Record<string, RPCMeta>
94
+ internalMeta: Record<string, string>
95
+ internalFiles: Map<string, { path: string; exportedName: string }>
96
+ exposedMeta: Record<string, string>
97
+ exposedFiles: Map<string, { path: string; exportedName: string }>
98
+ invokedFunctions: Set<string> // Track functions called via rpc.invoke()
73
99
  }
74
100
  mcpEndpoints: {
75
101
  resourcesMeta: MCPResourceMeta
@@ -77,4 +103,6 @@ export interface InspectorState {
77
103
  promptsMeta: MCPPromptMeta
78
104
  files: Set<string>
79
105
  }
106
+ middleware: InspectorMiddlewareState
107
+ permissions: InspectorPermissionState
80
108
  }
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as ts from 'typescript'
2
2
  import { InspectorFilters, InspectorLogger } from './types.js'
3
- import { PikkuWiringTypes } from '@pikku/core'
3
+ import { PikkuWiringTypes, FunctionServicesMeta } from '@pikku/core'
4
4
 
5
5
  type ExtractedFunctionName = {
6
6
  pikkuFuncName: string
@@ -289,7 +289,7 @@ export function extractFunctionName(
289
289
  const decls = resolvedSym.declarations ?? []
290
290
  if (decls.length > 0) {
291
291
  const decl = decls[0]!
292
- // Check if the declaration is a variable that uses pikkuSessionlessFunc
292
+ // Check if the declaration is a variable that uses a pikkuFun
293
293
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
294
294
  if (
295
295
  ts.isCallExpression(decl.initializer) &&
@@ -321,6 +321,8 @@ export function extractFunctionName(
321
321
  ts.isIdentifier(decl.name)
322
322
  ) {
323
323
  result.exportedName = decl.name.text
324
+ // CRITICAL FIX: Use exported name as pikkuFuncName for consistency
325
+ result.pikkuFuncName = decl.name.text
324
326
  } else if (ts.isIdentifier(decl.name)) {
325
327
  // If not exported, still capture the variable name
326
328
  result.localName = decl.name.text
@@ -717,6 +719,12 @@ export function extractFunctionName(
717
719
 
718
720
  // Apply name priority logic
719
721
  populateNameByPriority(result)
722
+
723
+ // CRITICAL FIX: If we have an exported name, use it as the pikkuFuncName for consistent lookup
724
+ if (result.exportedName && !result.explicitName) {
725
+ result.pikkuFuncName = result.exportedName
726
+ }
727
+
720
728
  return result
721
729
  }
722
730
 
@@ -910,3 +918,36 @@ export const matchesFilters = (
910
918
 
911
919
  return true
912
920
  }
921
+
922
+ /**
923
+ * Extract services from a function's first parameter destructuring pattern
924
+ */
925
+ export function extractServicesFromFunction(
926
+ handlerNode: ts.FunctionExpression | ts.ArrowFunction
927
+ ): FunctionServicesMeta {
928
+ const services: FunctionServicesMeta = {
929
+ optimized: true,
930
+ services: [],
931
+ }
932
+
933
+ const firstParam = handlerNode.parameters[0]
934
+ if (firstParam) {
935
+ if (ts.isObjectBindingPattern(firstParam.name)) {
936
+ for (const elem of firstParam.name.elements) {
937
+ const original =
938
+ elem.propertyName && ts.isIdentifier(elem.propertyName)
939
+ ? elem.propertyName.text
940
+ : ts.isIdentifier(elem.name)
941
+ ? elem.name.text
942
+ : undefined
943
+ if (original) {
944
+ services.services.push(original)
945
+ }
946
+ }
947
+ } else if (ts.isIdentifier(firstParam.name)) {
948
+ services.optimized = false
949
+ }
950
+ }
951
+
952
+ return services
953
+ }
package/src/visit.ts CHANGED
@@ -10,6 +10,9 @@ import { addMCPPrompt } from './add-mcp-prompt.js'
10
10
  import { InspectorFilters, InspectorState, InspectorLogger } from './types.js'
11
11
  import { addFunctions } from './add-functions.js'
12
12
  import { addChannel } from './add-channel.js'
13
+ import { addRPCInvocations } from './add-rpc-invocations.js'
14
+ import { addMiddleware } from './add-middleware.js'
15
+ import { addPermission } from './add-permission.js'
13
16
 
14
17
  export const visitSetup = (
15
18
  checker: ts.TypeChecker,
@@ -54,7 +57,7 @@ export const visitSetup = (
54
57
  )
55
58
 
56
59
  addFileWithFactory(node, checker, state.configFactories, 'CreateConfig')
57
- addFunctions(node, checker, state, filters, logger)
60
+ addRPCInvocations(node, state, logger)
58
61
 
59
62
  ts.forEachChild(node, (child) =>
60
63
  visitSetup(checker, child, state, filters, logger)
@@ -68,6 +71,7 @@ export const visitRoutes = (
68
71
  filters: InspectorFilters,
69
72
  logger: InspectorLogger
70
73
  ) => {
74
+ addFunctions(node, checker, state, logger)
71
75
  addHTTPRoute(node, checker, state, filters, logger)
72
76
  addSchedule(node, checker, state, filters, logger)
73
77
  addQueueWorker(node, checker, state, filters, logger)
@@ -75,6 +79,9 @@ export const visitRoutes = (
75
79
  addMCPResource(node, checker, state, filters, logger)
76
80
  addMCPTool(node, checker, state, filters, logger)
77
81
  addMCPPrompt(node, checker, state, filters, logger)
82
+ addMiddleware(node, checker, state, logger)
83
+ addPermission(node, checker, state, logger)
84
+
78
85
  ts.forEachChild(node, (child) =>
79
86
  visitRoutes(checker, child, state, filters, logger)
80
87
  )