@pikku/inspector 0.10.0 → 0.10.2

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.
@@ -136,7 +136,15 @@ export function addMessagesRoutes(
136
136
  const init = getInitializerOf(routeElem)
137
137
  if (!init) continue
138
138
 
139
- const routeKey = routeElem.name!.getText()
139
+ // Get the route key, stripping quotes if it's a string literal
140
+ const routeName = routeElem.name
141
+ if (!routeName) continue
142
+
143
+ let routeKey = routeName.getText()
144
+ // For string literals like 'greet' or "greet", strip the quotes
145
+ if (ts.isStringLiteral(routeName)) {
146
+ routeKey = routeName.text
147
+ }
140
148
 
141
149
  // For shorthand properties, we need to resolve the identifier to its declaration
142
150
  if (ts.isShorthandPropertyAssignment(routeElem)) {
@@ -196,8 +204,17 @@ export function addMessagesRoutes(
196
204
  // Look up in the registry
197
205
  const fnMeta = state.functions.meta[handlerName]
198
206
  if (fnMeta) {
207
+ // Resolve middleware for this route
208
+ const routeTags = ts.isObjectLiteralExpression(init)
209
+ ? getPropertyTags(init, 'channel', channelKey, logger)
210
+ : undefined
211
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
212
+ ? resolveMiddleware(state, init, routeTags, checker)
213
+ : undefined
214
+
199
215
  result[channelKey]![routeKey] = {
200
216
  pikkuFuncName: handlerName,
217
+ middleware: routeMiddleware,
201
218
  }
202
219
  continue
203
220
  }
@@ -214,8 +231,17 @@ export function addMessagesRoutes(
214
231
  // Look up in the registry
215
232
  const fnMeta = state.functions.meta[handlerName]
216
233
  if (fnMeta) {
234
+ // Resolve middleware for this route
235
+ const routeTags = ts.isObjectLiteralExpression(init)
236
+ ? getPropertyTags(init, 'channel', channelKey, logger)
237
+ : undefined
238
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
239
+ ? resolveMiddleware(state, init, routeTags, checker)
240
+ : undefined
241
+
217
242
  result[channelKey]![routeKey] = {
218
243
  pikkuFuncName: handlerName,
244
+ middleware: routeMiddleware,
219
245
  }
220
246
  continue
221
247
  }
@@ -249,8 +275,24 @@ export function addMessagesRoutes(
249
275
 
250
276
  const fnMeta = state.functions.meta[handlerName]
251
277
  if (fnMeta) {
278
+ // Resolve middleware for this route
279
+ const routeTags = ts.isObjectLiteralExpression(init)
280
+ ? getPropertyTags(
281
+ init,
282
+ 'channel',
283
+ channelKey,
284
+ logger
285
+ )
286
+ : undefined
287
+ const routeMiddleware = ts.isObjectLiteralExpression(
288
+ init
289
+ )
290
+ ? resolveMiddleware(state, init, routeTags, checker)
291
+ : undefined
292
+
252
293
  result[channelKey]![routeKey] = {
253
294
  pikkuFuncName: handlerName,
295
+ middleware: routeMiddleware,
254
296
  }
255
297
  continue
256
298
  }
@@ -264,8 +306,24 @@ export function addMessagesRoutes(
264
306
 
265
307
  const fnMeta = state.functions.meta[handlerName]
266
308
  if (fnMeta) {
309
+ // Resolve middleware for this route
310
+ const routeTags = ts.isObjectLiteralExpression(init)
311
+ ? getPropertyTags(
312
+ init,
313
+ 'channel',
314
+ channelKey,
315
+ logger
316
+ )
317
+ : undefined
318
+ const routeMiddleware = ts.isObjectLiteralExpression(
319
+ init
320
+ )
321
+ ? resolveMiddleware(state, init, routeTags, checker)
322
+ : undefined
323
+
267
324
  result[channelKey]![routeKey] = {
268
325
  pikkuFuncName: handlerName,
326
+ middleware: routeMiddleware,
269
327
  }
270
328
  continue
271
329
  }
@@ -336,8 +394,17 @@ export function addMessagesRoutes(
336
394
  const fnMeta = state.functions.meta[handlerName]
337
395
 
338
396
  if (fnMeta) {
397
+ // Resolve middleware for this route
398
+ const routeTags = ts.isObjectLiteralExpression(init)
399
+ ? getPropertyTags(init, 'channel', channelKey, logger)
400
+ : undefined
401
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
402
+ ? resolveMiddleware(state, init, routeTags, checker)
403
+ : undefined
404
+
339
405
  result[channelKey]![routeKey] = {
340
406
  pikkuFuncName: handlerName,
407
+ middleware: routeMiddleware,
341
408
  }
342
409
  continue // Skip the normal processing below
343
410
  }
@@ -368,8 +435,18 @@ export function addMessagesRoutes(
368
435
  continue
369
436
  }
370
437
 
438
+ // Resolve middleware and permissions for this route
439
+ // Check if the route config is an object literal with middleware/permissions
440
+ const routeTags = ts.isObjectLiteralExpression(init)
441
+ ? getPropertyTags(init, 'channel', channelKey, logger)
442
+ : undefined
443
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
444
+ ? resolveMiddleware(state, init, routeTags, checker)
445
+ : undefined
446
+
371
447
  result[channelKey]![routeKey] = {
372
448
  pikkuFuncName: handlerName,
449
+ middleware: routeMiddleware,
373
450
  }
374
451
  }
375
452
  }
@@ -417,13 +494,13 @@ export const addChannel: AddWiring = (
417
494
  const connect = getPropertyAssignmentInitializer(
418
495
  obj,
419
496
  'onConnect',
420
- false,
497
+ true,
421
498
  checker
422
499
  )
423
500
  const disconnect = getPropertyAssignmentInitializer(
424
501
  obj,
425
502
  'onDisconnect',
426
- false,
503
+ true,
427
504
  checker
428
505
  )
429
506
 
@@ -432,28 +509,26 @@ export const addChannel: AddWiring = (
432
509
  const onMsgProp = getPropertyAssignmentInitializer(
433
510
  obj,
434
511
  'onMessage',
435
- false,
512
+ true,
436
513
  checker
437
514
  )
438
515
 
439
516
  if (onMsgProp) {
440
- const handlerName =
441
- onMsgProp &&
442
- getHandlerNameFromExpression(onMsgProp, checker, state.rootDir)
443
- const fnMeta = handlerName && state.functions.meta[handlerName]
517
+ const { pikkuFuncName } = extractFunctionName(
518
+ onMsgProp,
519
+ checker,
520
+ state.rootDir
521
+ )
522
+ const fnMeta = state.functions.meta[pikkuFuncName]
444
523
  if (!fnMeta) {
445
- console.error(
446
- `No function metadata for onMessage handler '${handlerName}'`
524
+ logger.critical(
525
+ ErrorCode.FUNCTION_METADATA_NOT_FOUND,
526
+ `No function metadata found for onMessage handler '${pikkuFuncName}'`
447
527
  )
448
- throw new Error()
449
- } else {
450
- message = {
451
- pikkuFuncName: extractFunctionName(
452
- onMsgProp as any,
453
- checker,
454
- state.rootDir
455
- ).pikkuFuncName,
456
- }
528
+ return
529
+ }
530
+ message = {
531
+ pikkuFuncName,
457
532
  }
458
533
  }
459
534
 
@@ -2,6 +2,13 @@ import * as ts from 'typescript'
2
2
  import { PathToNameAndType, InspectorState } from '../types.js'
3
3
  import { extractServicesFromFunction } from '../utils/extract-services.js'
4
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
+ }
11
+
5
12
  export const addFileWithFactory = (
6
13
  node: ts.Node,
7
14
  checker: ts.TypeChecker,
@@ -14,6 +21,68 @@ export const addFileWithFactory = (
14
21
  const variableTypeNode = node.type
15
22
  const variableName = node.name.getText()
16
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
+
17
86
  if (variableTypeNode && ts.isTypeReferenceNode(variableTypeNode)) {
18
87
  const typeNameNode = variableTypeNode.typeName || null
19
88
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { inspect } from './inspector.js'
1
+ export { inspect, getInitialInspectorState } from './inspector.js'
2
2
  export { getFilesAndMethods } from './utils/get-files-and-methods.js'
3
3
  export type { TypesMap } from './types-map.js'
4
4
  export type * from './types.js'
package/src/inspector.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript'
2
+ import { performance } from 'perf_hooks'
2
3
  import { visitSetup, visitRoutes } from './visit.js'
3
4
  import { TypesMap } from './types-map.js'
4
5
  import { InspectorState, InspectorLogger, InspectorOptions } from './types.js'
@@ -6,22 +7,13 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js'
6
7
  import { findCommonAncestor } from './utils/find-root-dir.js'
7
8
  import { aggregateRequiredServices } from './utils/post-process.js'
8
9
 
9
- export const inspect = (
10
- logger: InspectorLogger,
11
- routeFiles: string[],
12
- options: InspectorOptions = {}
13
- ): InspectorState => {
14
- const program = ts.createProgram(routeFiles, {
15
- target: ts.ScriptTarget.ESNext,
16
- module: ts.ModuleKind.CommonJS,
17
- })
18
- const checker = program.getTypeChecker()
19
- const sourceFiles = program.getSourceFiles()
20
-
21
- // Infer root directory from source files
22
- const rootDir = findCommonAncestor(routeFiles)
23
-
24
- const state: InspectorState = {
10
+ /**
11
+ * Creates an initial/empty inspector state with all required properties initialized
12
+ * @param rootDir - The root directory for the project
13
+ * @returns A fresh InspectorState with empty collections
14
+ */
15
+ export function getInitialInspectorState(rootDir: string): InspectorState {
16
+ return {
25
17
  rootDir,
26
18
  singletonServicesTypeImportMap: new Map(),
27
19
  sessionServicesTypeImportMap: new Map(),
@@ -99,30 +91,89 @@ export const inspect = (
99
91
  usedFunctions: new Set(),
100
92
  usedMiddleware: new Set(),
101
93
  usedPermissions: new Set(),
94
+ allSingletonServices: [],
95
+ allSessionServices: [],
102
96
  },
103
97
  }
98
+ }
99
+
100
+ export const inspect = (
101
+ logger: InspectorLogger,
102
+ routeFiles: string[],
103
+ options: InspectorOptions = {}
104
+ ): InspectorState => {
105
+ const startProgram = performance.now()
106
+ const program = ts.createProgram(routeFiles, {
107
+ target: ts.ScriptTarget.ESNext,
108
+ module: ts.ModuleKind.CommonJS,
109
+ skipLibCheck: true,
110
+ skipDefaultLibCheck: true,
111
+ moduleResolution: ts.ModuleResolutionKind.Node10,
112
+ types: [],
113
+ allowJs: false,
114
+ checkJs: false,
115
+ })
116
+ logger.debug(
117
+ `Created program in ${(performance.now() - startProgram).toFixed(2)}ms`
118
+ )
119
+
120
+ const startChecker = performance.now()
121
+ const checker = program.getTypeChecker()
122
+ logger.debug(
123
+ `Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`
124
+ )
125
+
126
+ const startSourceFiles = performance.now()
127
+ const sourceFiles = program.getSourceFiles()
128
+ logger.debug(
129
+ `Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`
130
+ )
131
+
132
+ // Infer root directory from source files
133
+ const rootDir = findCommonAncestor(routeFiles)
134
+
135
+ const state = getInitialInspectorState(rootDir)
104
136
 
105
137
  // First sweep: add all functions
138
+ const startSetup = performance.now()
106
139
  for (const sourceFile of sourceFiles) {
107
140
  ts.forEachChild(sourceFile, (child) =>
108
141
  visitSetup(logger, checker, child, state, options)
109
142
  )
110
143
  }
144
+ logger.debug(
145
+ `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
146
+ )
111
147
 
112
- // Second sweep: add all transports
113
- for (const sourceFile of sourceFiles) {
114
- ts.forEachChild(sourceFile, (child) =>
115
- visitRoutes(logger, checker, child, state, options)
148
+ if (!options.setupOnly) {
149
+ // Second sweep: add all transports
150
+ const startRoutes = performance.now()
151
+ for (const sourceFile of sourceFiles) {
152
+ ts.forEachChild(sourceFile, (child) =>
153
+ visitRoutes(logger, checker, child, state, options)
154
+ )
155
+ }
156
+ logger.debug(
157
+ `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`
116
158
  )
117
159
  }
118
160
 
119
161
  // Populate filesAndMethods
162
+ const startFilesAndMethods = performance.now()
120
163
  const { result, errors } = getFilesAndMethods(state, options.types)
121
164
  state.filesAndMethods = result
122
165
  state.filesAndMethodsErrors = errors
166
+ logger.debug(
167
+ `Get files and methods completed in ${(performance.now() - startFilesAndMethods).toFixed(2)}ms`
168
+ )
123
169
 
124
- // Post-processing: Aggregate required services from wired functions/middleware/permissions
125
- aggregateRequiredServices(state)
170
+ if (!options.setupOnly) {
171
+ const startAggregate = performance.now()
172
+ aggregateRequiredServices(state)
173
+ logger.debug(
174
+ `Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
175
+ )
176
+ }
126
177
 
127
178
  return state
128
179
  }
package/src/types.ts CHANGED
@@ -114,6 +114,7 @@ export type InspectorFilters = {
114
114
  }
115
115
 
116
116
  export type InspectorOptions = Partial<{
117
+ setupOnly: boolean
117
118
  types: Partial<{
118
119
  configFileType: string
119
120
  userSessionType: string
@@ -231,5 +232,7 @@ export interface InspectorState {
231
232
  usedFunctions: Set<string> // Function names actually wired/exposed
232
233
  usedMiddleware: Set<string> // Middleware names used by wired functions
233
234
  usedPermissions: Set<string> // Permission names used by wired functions
235
+ allSingletonServices: string[] // All services available in SingletonServices type
236
+ allSessionServices: string[] // All services available in Services type (excluding SingletonServices)
234
237
  }
235
238
  }
@@ -54,9 +54,9 @@ const getMetaTypes = (
54
54
  const helpMessage =
55
55
  type === 'CoreConfig'
56
56
  ? `No ${type} found. Make sure you have exported a createConfig function in your codebase:\n\n` +
57
- `export const createConfig: CreateConfig<Config> = async () => {\n` +
57
+ `export const createConfig = pikkuConfig(async () => {\n` +
58
58
  ` return {}\n` +
59
- `}\n\n` +
59
+ `})\n\n` +
60
60
  `Possible issues:\n` +
61
61
  `- srcDirectories in pikku.config.json doesn't include the file with the createConfig method`
62
62
  : `No ${type} found`
@@ -4,6 +4,7 @@ import {
4
4
  MiddlewareMetadata,
5
5
  PermissionMetadata,
6
6
  } from '@pikku/core'
7
+ import { extractTypeKeys } from './type-utils.js'
7
8
 
8
9
  /**
9
10
  * Helper to extract wire-level middleware/permission names from metadata.
@@ -28,7 +29,7 @@ export function extractWireNames(
28
29
  */
29
30
  function expandAndAddGroupServices(
30
31
  list: MiddlewareMetadata[] | PermissionMetadata[] | undefined,
31
- state: InspectorState,
32
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>,
32
33
  addServices: (services: FunctionServicesMeta | undefined) => void,
33
34
  isMiddleware: boolean
34
35
  ): void {
@@ -57,6 +58,38 @@ function expandAndAddGroupServices(
57
58
  }
58
59
  }
59
60
 
61
+ /**
62
+ * Extracts all service names from SingletonServices and Services types.
63
+ * This provides the complete list of available services for code generation.
64
+ * Only runs if typesLookup is available (omitted in deserialized states).
65
+ */
66
+ function extractAllServices(
67
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
68
+ ): void {
69
+ // Skip if typesLookup is not available (e.g., deserialized state)
70
+ if (!('typesLookup' in state)) {
71
+ return
72
+ }
73
+
74
+ // Extract all singleton services from the SingletonServices type
75
+ const singletonServicesTypes = state.typesLookup.get('SingletonServices')
76
+ if (singletonServicesTypes && singletonServicesTypes.length > 0) {
77
+ const singletonServiceNames = extractTypeKeys(singletonServicesTypes[0])
78
+ state.serviceAggregation.allSingletonServices = singletonServiceNames.sort()
79
+ }
80
+
81
+ // Extract all services from the Services type
82
+ const servicesTypes = state.typesLookup.get('Services')
83
+ if (servicesTypes && servicesTypes.length > 0) {
84
+ const allServiceNames = extractTypeKeys(servicesTypes[0])
85
+ // Session services are those in Services but not in SingletonServices
86
+ const singletonSet = new Set(state.serviceAggregation.allSingletonServices)
87
+ state.serviceAggregation.allSessionServices = allServiceNames
88
+ .filter((name) => !singletonSet.has(name))
89
+ .sort()
90
+ }
91
+ }
92
+
60
93
  /**
61
94
  * Aggregates all required services from wired functions, middleware, and permissions.
62
95
  * Must be called after AST traversal completes.
@@ -64,7 +97,12 @@ function expandAndAddGroupServices(
64
97
  * Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
65
98
  * in the add-* methods during AST traversal for efficiency.
66
99
  */
67
- export function aggregateRequiredServices(state: InspectorState): void {
100
+ export function aggregateRequiredServices(
101
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
102
+ ): void {
103
+ // First, extract all available services from types
104
+ extractAllServices(state)
105
+
68
106
  const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } =
69
107
  state.serviceAggregation
70
108
 
@@ -162,6 +162,8 @@ export interface SerializableInspectorState {
162
162
  usedFunctions: string[]
163
163
  usedMiddleware: string[]
164
164
  usedPermissions: string[]
165
+ allSingletonServices: string[]
166
+ allSessionServices: string[]
165
167
  }
166
168
  }
167
169
 
@@ -271,6 +273,8 @@ export function serializeInspectorState(
271
273
  usedFunctions: Array.from(state.serviceAggregation.usedFunctions),
272
274
  usedMiddleware: Array.from(state.serviceAggregation.usedMiddleware),
273
275
  usedPermissions: Array.from(state.serviceAggregation.usedPermissions),
276
+ allSingletonServices: state.serviceAggregation.allSingletonServices,
277
+ allSessionServices: state.serviceAggregation.allSessionServices,
274
278
  },
275
279
  }
276
280
  }
@@ -370,6 +374,8 @@ export function deserializeInspectorState(
370
374
  usedFunctions: new Set(data.serviceAggregation.usedFunctions),
371
375
  usedMiddleware: new Set(data.serviceAggregation.usedMiddleware),
372
376
  usedPermissions: new Set(data.serviceAggregation.usedPermissions),
377
+ allSingletonServices: data.serviceAggregation.allSingletonServices,
378
+ allSessionServices: data.serviceAggregation.allSessionServices,
373
379
  },
374
380
  }
375
381
  }
@@ -37,20 +37,22 @@ export function getPropertyAssignmentInitializer(
37
37
  ) {
38
38
  if (!checker) return prop.name // best effort without a checker
39
39
 
40
- let sym = checker.getSymbolAtLocation(prop.name)
40
+ // Use the proper TypeScript API for shorthand property resolution
41
+ let sym = checker.getShorthandAssignmentValueSymbol(prop)
41
42
  if (sym && sym.flags & ts.SymbolFlags.Alias) {
42
43
  sym = checker.getAliasedSymbol(sym)
43
44
  }
44
45
 
45
46
  const decl = sym?.declarations?.[0]
46
47
 
47
- // const foo = () => {}
48
+ // const foo = () => {} or const foo = pikkuFunc(...)
48
49
  if (
49
50
  decl &&
50
51
  ts.isVariableDeclaration(decl) &&
51
52
  decl.initializer &&
52
53
  (ts.isArrowFunction(decl.initializer) ||
53
- ts.isFunctionExpression(decl.initializer))
54
+ ts.isFunctionExpression(decl.initializer) ||
55
+ ts.isCallExpression(decl.initializer))
54
56
  ) {
55
57
  return decl.initializer
56
58
  }
package/src/visit.ts CHANGED
@@ -70,8 +70,8 @@ export const visitSetup = (
70
70
  )
71
71
 
72
72
  addFileWithFactory(node, checker, state.configFactories, 'CreateConfig')
73
- addRPCInvocations(node, state, logger)
74
73
 
74
+ addRPCInvocations(node, state, logger)
75
75
  addMiddleware(logger, node, checker, state, options)
76
76
  addPermission(logger, node, checker, state, options)
77
77