@pikku/inspector 0.12.22 → 0.12.23

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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/add/add-addon-bans.d.ts +7 -0
  3. package/dist/add/add-addon-bans.js +65 -0
  4. package/dist/add/add-channel.js +47 -6
  5. package/dist/add/add-cli.js +17 -0
  6. package/dist/add/add-http-route.d.ts +11 -1
  7. package/dist/add/add-http-route.js +37 -0
  8. package/dist/add/add-http-routes.d.ts +0 -3
  9. package/dist/add/add-http-routes.js +179 -36
  10. package/dist/error-codes.d.ts +3 -1
  11. package/dist/error-codes.js +3 -0
  12. package/dist/inspector.js +17 -5
  13. package/dist/types.d.ts +43 -1
  14. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  15. package/dist/utils/get-exported-variable-name.js +34 -0
  16. package/dist/utils/load-addon-functions-meta.js +98 -0
  17. package/dist/utils/resolve-addon-package.js +3 -1
  18. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  19. package/dist/utils/resolve-ref-contract.js +46 -0
  20. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  21. package/dist/utils/serialize-inspector-state.js +9 -0
  22. package/dist/visit.js +24 -19
  23. package/package.json +1 -1
  24. package/src/add/add-addon-bans.ts +84 -0
  25. package/src/add/add-channel.ts +66 -7
  26. package/src/add/add-cli.ts +30 -0
  27. package/src/add/add-http-route.ts +75 -1
  28. package/src/add/add-http-routes.ts +283 -41
  29. package/src/add/addon-bans.test.ts +121 -0
  30. package/src/add/addon-contracts.test.ts +221 -0
  31. package/src/error-codes.ts +4 -0
  32. package/src/inspector.ts +17 -5
  33. package/src/types.ts +65 -1
  34. package/src/utils/get-exported-variable-name.ts +48 -0
  35. package/src/utils/load-addon-functions-meta.ts +164 -0
  36. package/src/utils/resolve-addon-package.ts +6 -1
  37. package/src/utils/resolve-ref-contract.ts +71 -0
  38. package/src/utils/serialize-inspector-state.ts +10 -0
  39. package/src/visit.ts +26 -19
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -21,6 +21,8 @@ import { resolveIdentifier } from '../utils/resolve-identifier.js'
21
21
  import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
22
22
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
23
23
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
24
+ import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
25
+ import { resolveRefContract } from '../utils/resolve-ref-contract.js'
24
26
 
25
27
  /**
26
28
  * Safely get the "initializer" expression of a property-like AST node:
@@ -40,6 +42,16 @@ function getInitializerOf(
40
42
  return undefined
41
43
  }
42
44
 
45
+ function getObjectPropertyName(
46
+ name: ts.PropertyName | undefined
47
+ ): string | null {
48
+ if (!name) return null
49
+ if (ts.isIdentifier(name)) return name.text
50
+ if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text
51
+ if (ts.isComputedPropertyName(name)) return null
52
+ return name.getText()
53
+ }
54
+
43
55
  /**
44
56
  * Resolve a handler expression (Identifier, CallExpression, or { func })
45
57
  * into its underlying function name.
@@ -116,6 +128,27 @@ function getHandlerNameFromExpression(
116
128
  return null
117
129
  }
118
130
 
131
+ function extractExportedChannelRoutes(
132
+ logger: {
133
+ error: (msg: string) => void
134
+ critical: (code: ErrorCode, msg: string) => void
135
+ },
136
+ routes: ts.ObjectLiteralExpression,
137
+ state: InspectorState,
138
+ checker: ts.TypeChecker
139
+ ): Record<string, ChannelMessageMeta> {
140
+ const wrapper = ts.factory.createObjectLiteralExpression([
141
+ ts.factory.createPropertyAssignment(
142
+ 'onMessageWiring',
143
+ ts.factory.createObjectLiteralExpression([
144
+ ts.factory.createPropertyAssignment('contract', routes),
145
+ ])
146
+ ),
147
+ ])
148
+
149
+ return addMessagesRoutes(logger, wrapper, state, checker).contract ?? {}
150
+ }
151
+
119
152
  /**
120
153
  * Build out the nested message-routes by looking up each handler
121
154
  * in state.functions.meta instead of re-inferring it here.
@@ -155,9 +188,24 @@ export function addMessagesRoutes(
155
188
  }
156
189
  }
157
190
 
191
+ const refContract = resolveRefContract(
192
+ chanInit,
193
+ 'refChannel',
194
+ state.exportedContracts.addonChannel
195
+ )
196
+ if (refContract) {
197
+ const refChannelKey = getObjectPropertyName(chanElem.name)
198
+ if (!refChannelKey) continue
199
+ result[refChannelKey] = {
200
+ ...refContract.contract,
201
+ }
202
+ continue
203
+ }
204
+
158
205
  if (!ts.isObjectLiteralExpression(chanInit)) continue
159
206
 
160
- const channelKey = chanElem.name!.getText()
207
+ const channelKey = getObjectPropertyName(chanElem.name)
208
+ if (!channelKey) continue
161
209
  result[channelKey] = {}
162
210
 
163
211
  for (const routeElem of chanInit.properties) {
@@ -168,11 +216,8 @@ export function addMessagesRoutes(
168
216
  const routeName = routeElem.name
169
217
  if (!routeName) continue
170
218
 
171
- let routeKey = routeName.getText()
172
- // For string literals like 'greet' or "greet", strip the quotes
173
- if (ts.isStringLiteral(routeName)) {
174
- routeKey = routeName.text
175
- }
219
+ const routeKey = getObjectPropertyName(routeName)
220
+ if (!routeKey) continue
176
221
 
177
222
  // For shorthand properties, we need to resolve the identifier to its declaration
178
223
  if (ts.isShorthandPropertyAssignment(routeElem)) {
@@ -529,6 +574,18 @@ export const addChannel: AddWiring = (
529
574
  options
530
575
  ) => {
531
576
  if (!ts.isCallExpression(node)) return
577
+ if (
578
+ ts.isIdentifier(node.expression) &&
579
+ node.expression.text === 'defineChannelRoutes'
580
+ ) {
581
+ const exportName = getExportedVariableName(node, options.sourceFile)
582
+ const [firstArg] = node.arguments
583
+ if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
584
+ state.exportedContracts.channel[exportName] =
585
+ extractExportedChannelRoutes(logger, firstArg, state, checker)
586
+ }
587
+ return
588
+ }
532
589
  const { expression, arguments: args } = node
533
590
  if (!ts.isIdentifier(expression) || expression.text !== 'wireChannel') return
534
591
  const first = args[0]
@@ -664,7 +721,9 @@ export const addChannel: AddWiring = (
664
721
  state.serviceAggregation.usedFunctions.add(message.pikkuFuncId)
665
722
  }
666
723
 
667
- for (const channelHandlers of Object.values(messageWirings)) {
724
+ for (const channelHandlers of Object.values(
725
+ messageWirings as Record<string, Record<string, ChannelMessageMeta>>
726
+ )) {
668
727
  for (const handler of Object.values(channelHandlers)) {
669
728
  state.serviceAggregation.usedFunctions.add(handler.pikkuFuncId)
670
729
  }
@@ -19,6 +19,8 @@ import { resolveIdentifier } from '../utils/resolve-identifier.js'
19
19
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
20
20
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
21
21
  import { extractServicesFromFunction } from '../utils/extract-services.js'
22
+ import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
23
+ import { resolveRefContract } from '../utils/resolve-ref-contract.js'
22
24
 
23
25
  // Track if we've warned about missing Config type to avoid duplicate warnings
24
26
  const configTypeWarningShown = new Set<string>()
@@ -34,6 +36,25 @@ export const addCLI: AddWiring = (
34
36
  options
35
37
  ) => {
36
38
  if (!ts.isCallExpression(node)) return
39
+ if (
40
+ ts.isIdentifier(node.expression) &&
41
+ node.expression.text === 'defineCLICommands'
42
+ ) {
43
+ const exportName = getExportedVariableName(node, options.sourceFile)
44
+ const [firstArg] = node.arguments
45
+ if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
46
+ inspectorState.exportedContracts.cli[exportName] = processCommands(
47
+ logger,
48
+ firstArg,
49
+ node.getSourceFile(),
50
+ typeChecker,
51
+ exportName,
52
+ inspectorState,
53
+ options
54
+ )
55
+ }
56
+ return
57
+ }
37
58
  // Check if this is a wireCLI call
38
59
  if (!node || !node.expression) {
39
60
  return
@@ -214,6 +235,15 @@ function processCommands(
214
235
  programTags
215
236
  )
216
237
  Object.assign(commands, spreadCommands)
238
+ } else {
239
+ const refCommands = resolveRefContract(
240
+ prop.expression,
241
+ 'refCLI',
242
+ inspectorState.exportedContracts.addonCli
243
+ )
244
+ if (refCommands) {
245
+ Object.assign(commands, refCommands.contract)
246
+ }
217
247
  }
218
248
  continue
219
249
  }
@@ -13,7 +13,11 @@ import {
13
13
  getPropertyAssignmentInitializer,
14
14
  extractTypeKeys,
15
15
  } from '../utils/type-utils.js'
16
- import type { AddWiring, InspectorState } from '../types.js'
16
+ import type {
17
+ AddWiring,
18
+ ExportedHTTPRouteConfigMeta,
19
+ InspectorState,
20
+ } from '../types.js'
17
21
  import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js'
18
22
  import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js'
19
23
  import { extractWireNames } from '../utils/post-process.js'
@@ -40,6 +44,16 @@ export interface RegisterHTTPRouteParams {
40
44
  inheritedAuth?: boolean
41
45
  }
42
46
 
47
+ export interface RegisterHTTPRouteMetaParams {
48
+ route: ExportedHTTPRouteConfigMeta
49
+ state: InspectorState
50
+ logger: InspectorLogger
51
+ sourceFile: ts.SourceFile
52
+ basePath?: string
53
+ inheritedTags?: string[]
54
+ inheritedAuth?: boolean
55
+ }
56
+
43
57
  /**
44
58
  * Extract header schema reference from headers property
45
59
  */
@@ -421,6 +435,66 @@ export function registerHTTPRoute({
421
435
  }
422
436
  }
423
437
 
438
+ export function registerHTTPRouteMeta({
439
+ route,
440
+ state,
441
+ logger,
442
+ sourceFile,
443
+ basePath = '',
444
+ inheritedTags = [],
445
+ inheritedAuth,
446
+ }: RegisterHTTPRouteMetaParams): void {
447
+ const method = route.method.toLowerCase()
448
+ const fullRoute = basePath + route.route
449
+ const tags = [...inheritedTags, ...(route.tags || [])]
450
+ const funcName = route.func.pikkuFuncId
451
+ const fnMeta = resolveFunctionMeta(state, funcName)
452
+
453
+ if (!fnMeta) {
454
+ logger.critical(
455
+ ErrorCode.FUNCTION_METADATA_NOT_FOUND,
456
+ `No function metadata found for '${funcName}'.`
457
+ )
458
+ return
459
+ }
460
+
461
+ let params: string[] = []
462
+ try {
463
+ const keys = pathToRegexp(fullRoute).keys
464
+ params = keys.filter((k) => k.type === 'param').map((k) => k.name)
465
+ } catch (error) {
466
+ logger.error(
467
+ `Failed to parse route '${fullRoute}': ${error instanceof Error ? error.message : error}`
468
+ )
469
+ return
470
+ }
471
+
472
+ if (!route.func.packageName) {
473
+ computeInputTypes(
474
+ state.http.metaInputTypes,
475
+ method,
476
+ fnMeta.inputs?.[0] || null,
477
+ [],
478
+ params
479
+ )
480
+ }
481
+
482
+ state.serviceAggregation.usedFunctions.add(funcName)
483
+ state.http.files.add(sourceFile.fileName)
484
+ state.http.meta[method][fullRoute] = {
485
+ pikkuFuncId: funcName,
486
+ ...(route.func.packageName && { packageName: route.func.packageName }),
487
+ route: fullRoute,
488
+ sourceFile: sourceFile.fileName,
489
+ method: method as HTTPMethod,
490
+ params: params.length > 0 ? params : undefined,
491
+ inputTypes: undefined,
492
+ tags: tags.length > 0 ? tags : undefined,
493
+ sse: route.sse ? true : undefined,
494
+ groupBasePath: basePath || undefined,
495
+ }
496
+ }
497
+
424
498
  /**
425
499
  * Process wireHTTP calls
426
500
  */
@@ -1,45 +1,63 @@
1
1
  import * as ts from 'typescript'
2
2
  import { getPropertyValue } from '../utils/get-property-value.js'
3
- import type { AddWiring, InspectorState, InspectorLogger } from '../types.js'
4
- import { registerHTTPRoute } from './add-http-route.js'
3
+ import type {
4
+ AddWiring,
5
+ ExportedHTTPRouteConfigMeta,
6
+ ExportedHTTPRouteMapMeta,
7
+ ExportedHTTPRoutesGroupMeta,
8
+ InspectorLogger,
9
+ InspectorState,
10
+ } from '../types.js'
11
+ import { registerHTTPRoute, registerHTTPRouteMeta } from './add-http-route.js'
5
12
  import { resolveIdentifier } from '../utils/resolve-identifier.js'
13
+ import { extractFunctionName } from '../utils/extract-function-name.js'
14
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
15
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
+ import {
17
+ resolveRefContract,
18
+ type RefContractResolution,
19
+ } from '../utils/resolve-ref-contract.js'
20
+ import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
6
21
 
7
- /**
8
- * Group configuration extracted from wireHTTPRoutes or defineHTTPRoutes
9
- */
10
22
  interface GroupConfig {
11
23
  basePath: string
12
24
  tags: string[]
13
25
  auth?: boolean
14
26
  }
15
27
 
16
- /**
17
- * Process wireHTTPRoutes calls
18
- */
19
28
  export const addHTTPRoutes: AddWiring = (
20
29
  logger,
21
30
  node,
22
31
  checker,
23
32
  state,
24
- _options
33
+ options
25
34
  ) => {
26
35
  if (!ts.isCallExpression(node)) return
27
36
 
28
37
  const { expression, arguments: args } = node
29
- if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTPRoutes')
38
+ if (!ts.isIdentifier(expression)) return
39
+
40
+ if (expression.text === 'defineHTTPRoutes') {
41
+ const exportName = getExportedVariableName(node, options.sourceFile)
42
+ const firstArg = args[0]
43
+ if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
44
+ const contract = serializeHTTPRoutesContract(firstArg, checker, state)
45
+ if (contract) {
46
+ state.exportedContracts.http[exportName] = contract
47
+ }
48
+ }
30
49
  return
50
+ }
51
+
52
+ if (expression.text !== 'wireHTTPRoutes') return
31
53
 
32
54
  const firstArg = args[0]
33
55
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
34
56
 
35
- // Extract group config
36
57
  const groupConfig = extractGroupConfig(firstArg)
37
-
38
- // Get routes property
39
58
  const routesProp = getPropertyAssignment(firstArg, 'routes')
40
59
  if (!routesProp) return
41
60
 
42
- // Process routes recursively
43
61
  processRoutes(
44
62
  routesProp.initializer,
45
63
  groupConfig,
@@ -50,9 +68,6 @@ export const addHTTPRoutes: AddWiring = (
50
68
  )
51
69
  }
52
70
 
53
- /**
54
- * Get a property assignment from an object literal
55
- */
56
71
  function getPropertyAssignment(
57
72
  obj: ts.ObjectLiteralExpression,
58
73
  propName: string
@@ -69,9 +84,6 @@ function getPropertyAssignment(
69
84
  return undefined
70
85
  }
71
86
 
72
- /**
73
- * Extract group configuration from an object literal
74
- */
75
87
  function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
76
88
  const basePath = (getPropertyValue(obj, 'basePath') as string) || ''
77
89
  const tags = (getPropertyValue(obj, 'tags') as string[]) || []
@@ -84,9 +96,6 @@ function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
84
96
  }
85
97
  }
86
98
 
87
- /**
88
- * Merge two group configs following cascading rules
89
- */
90
99
  function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
91
100
  return {
92
101
  basePath: parent.basePath + child.basePath,
@@ -95,9 +104,6 @@ function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
95
104
  }
96
105
  }
97
106
 
98
- /**
99
- * Check if a value is a route config (has method, func, and route)
100
- */
101
107
  function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
102
108
  let hasMethod = false
103
109
  let hasFunc = false
@@ -114,9 +120,6 @@ function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
114
120
  return hasMethod && hasFunc && hasRoute
115
121
  }
116
122
 
117
- /**
118
- * Check if a value is a route contract (has routes property but no method/func)
119
- */
120
123
  function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
121
124
  let hasRoutes = false
122
125
  let hasMethod = false
@@ -133,9 +136,6 @@ function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
133
136
  return hasRoutes && !hasMethod && !hasFunc
134
137
  }
135
138
 
136
- /**
137
- * Recursively process routes - handles nested maps, contracts, and identifiers
138
- */
139
139
  function processRoutes(
140
140
  node: ts.Node,
141
141
  parentConfig: GroupConfig,
@@ -144,7 +144,6 @@ function processRoutes(
144
144
  logger: InspectorLogger,
145
145
  sourceFile: ts.SourceFile
146
146
  ): void {
147
- // Handle array of routes
148
147
  if (ts.isArrayLiteralExpression(node)) {
149
148
  for (const element of node.elements) {
150
149
  if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
@@ -154,15 +153,12 @@ function processRoutes(
154
153
  return
155
154
  }
156
155
 
157
- // Handle object literal
158
156
  if (ts.isObjectLiteralExpression(node)) {
159
- // Check if this is a route config
160
157
  if (isRouteConfig(node)) {
161
158
  processRoute(node, parentConfig, state, checker, logger, sourceFile)
162
159
  return
163
160
  }
164
161
 
165
- // Check if this is a route contract
166
162
  if (isRouteContract(node)) {
167
163
  const contractConfig = extractGroupConfig(node)
168
164
  const mergedConfig = mergeConfigs(parentConfig, contractConfig)
@@ -180,9 +176,17 @@ function processRoutes(
180
176
  return
181
177
  }
182
178
 
183
- // Otherwise it's a nested map - process each property
184
179
  for (const prop of node.properties) {
185
180
  if (ts.isPropertyAssignment(prop)) {
181
+ const ref = resolveRefContract(
182
+ prop.initializer,
183
+ 'refHTTP',
184
+ state.exportedContracts.addonHttp
185
+ )
186
+ if (ref) {
187
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
188
+ continue
189
+ }
186
190
  processRoutes(
187
191
  prop.initializer,
188
192
  parentConfig,
@@ -196,7 +200,18 @@ function processRoutes(
196
200
  return
197
201
  }
198
202
 
199
- // Handle identifier - resolve to its definition
203
+ if (ts.isCallExpression(node)) {
204
+ const ref = resolveRefContract(
205
+ node,
206
+ 'refHTTP',
207
+ state.exportedContracts.addonHttp
208
+ )
209
+ if (ref) {
210
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
211
+ }
212
+ return
213
+ }
214
+
200
215
  if (ts.isIdentifier(node)) {
201
216
  const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes'])
202
217
  if (resolved) {
@@ -205,9 +220,236 @@ function processRoutes(
205
220
  }
206
221
  }
207
222
 
208
- /**
209
- * Register a single route using the shared registerHTTPRoute function
210
- */
223
+ function processRefHTTPContract(
224
+ ref: RefContractResolution<ExportedHTTPRoutesGroupMeta>,
225
+ parentConfig: GroupConfig,
226
+ state: InspectorState,
227
+ logger: InspectorLogger,
228
+ sourceFile: ts.SourceFile
229
+ ): void {
230
+ const basePath =
231
+ ref.basePath !== undefined ? ref.basePath : ref.contract.basePath || ''
232
+ processExportedRouteMap(
233
+ ref.contract.routes,
234
+ mergeConfigs(parentConfig, {
235
+ basePath,
236
+ tags: ref.contract.tags || [],
237
+ auth: ref.contract.auth,
238
+ }),
239
+ state,
240
+ logger,
241
+ sourceFile
242
+ )
243
+ }
244
+
245
+ function processExportedRouteMap(
246
+ routes: ExportedHTTPRouteMapMeta,
247
+ parentConfig: GroupConfig,
248
+ state: InspectorState,
249
+ logger: InspectorLogger,
250
+ sourceFile: ts.SourceFile
251
+ ): void {
252
+ for (const value of Object.values(routes)) {
253
+ if (isExportedRouteConfig(value)) {
254
+ registerHTTPRouteMeta({
255
+ route: value,
256
+ state,
257
+ logger,
258
+ sourceFile,
259
+ basePath: parentConfig.basePath,
260
+ inheritedTags: parentConfig.tags,
261
+ inheritedAuth: parentConfig.auth,
262
+ })
263
+ continue
264
+ }
265
+
266
+ if (isExportedRouteContract(value)) {
267
+ processExportedRouteMap(
268
+ value.routes,
269
+ mergeConfigs(parentConfig, {
270
+ basePath: value.basePath || '',
271
+ tags: value.tags || [],
272
+ auth: value.auth,
273
+ }),
274
+ state,
275
+ logger,
276
+ sourceFile
277
+ )
278
+ continue
279
+ }
280
+
281
+ processExportedRouteMap(value, parentConfig, state, logger, sourceFile)
282
+ }
283
+ }
284
+
285
+ function isExportedRouteConfig(
286
+ value: ExportedHTTPRouteMapMeta[string]
287
+ ): value is ExportedHTTPRouteConfigMeta {
288
+ return (
289
+ typeof value === 'object' &&
290
+ value !== null &&
291
+ 'method' in value &&
292
+ 'route' in value &&
293
+ 'func' in value
294
+ )
295
+ }
296
+
297
+ function isExportedRouteContract(
298
+ value: ExportedHTTPRouteMapMeta[string]
299
+ ): value is ExportedHTTPRoutesGroupMeta {
300
+ return (
301
+ typeof value === 'object' &&
302
+ value !== null &&
303
+ 'routes' in value &&
304
+ !('method' in value)
305
+ )
306
+ }
307
+
308
+ function serializeHTTPRoutesContract(
309
+ node: ts.ObjectLiteralExpression,
310
+ checker: ts.TypeChecker,
311
+ state: InspectorState
312
+ ): ExportedHTTPRoutesGroupMeta | null {
313
+ if (isRouteContract(node)) {
314
+ const routesProp = getPropertyAssignment(node, 'routes')
315
+ if (!routesProp || !ts.isObjectLiteralExpression(routesProp.initializer)) {
316
+ return null
317
+ }
318
+
319
+ return {
320
+ ...extractGroupConfig(node),
321
+ routes: serializeHTTPRouteMap(routesProp.initializer, checker, state),
322
+ }
323
+ }
324
+
325
+ return {
326
+ routes: serializeHTTPRouteMap(node, checker, state),
327
+ }
328
+ }
329
+
330
+ function serializeHTTPRouteMap(
331
+ node: ts.ObjectLiteralExpression,
332
+ checker: ts.TypeChecker,
333
+ state: InspectorState
334
+ ): ExportedHTTPRouteMapMeta {
335
+ const result: ExportedHTTPRouteMapMeta = {}
336
+
337
+ for (const prop of node.properties) {
338
+ if (!ts.isPropertyAssignment(prop)) continue
339
+
340
+ const key = prop.name.getText().replace(/^['"]|['"]$/g, '')
341
+ const value = prop.initializer
342
+
343
+ if (ts.isObjectLiteralExpression(value)) {
344
+ if (isRouteConfig(value)) {
345
+ const route = serializeHTTPRouteConfig(value, checker, state)
346
+ if (route) {
347
+ result[key] = route
348
+ }
349
+ continue
350
+ }
351
+
352
+ if (isRouteContract(value)) {
353
+ const routeContract = serializeHTTPRoutesContract(value, checker, state)
354
+ if (routeContract) {
355
+ result[key] = routeContract
356
+ }
357
+ continue
358
+ }
359
+
360
+ result[key] = serializeHTTPRouteMap(value, checker, state)
361
+ continue
362
+ }
363
+
364
+ if (ts.isIdentifier(value)) {
365
+ const resolved = resolveIdentifier(value, checker, ['defineHTTPRoutes'])
366
+ if (resolved && ts.isObjectLiteralExpression(resolved)) {
367
+ if (isRouteContract(resolved)) {
368
+ const routeContract = serializeHTTPRoutesContract(
369
+ resolved,
370
+ checker,
371
+ state
372
+ )
373
+ if (routeContract) {
374
+ result[key] = routeContract
375
+ }
376
+ } else {
377
+ result[key] = serializeHTTPRouteMap(resolved, checker, state)
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ return result
384
+ }
385
+
386
+ function serializeHTTPRouteConfig(
387
+ obj: ts.ObjectLiteralExpression,
388
+ checker: ts.TypeChecker,
389
+ state: InspectorState
390
+ ): ExportedHTTPRouteConfigMeta | null {
391
+ const method = getPropertyValue(obj, 'method') as string | null
392
+ const route = getPropertyValue(obj, 'route') as string | null
393
+ const funcInitializer = getPropertyAssignmentInitializer(
394
+ obj,
395
+ 'func',
396
+ true,
397
+ checker
398
+ )
399
+
400
+ if (!method || !route || !funcInitializer) {
401
+ return null
402
+ }
403
+
404
+ let pikkuFuncId = extractFunctionName(
405
+ funcInitializer,
406
+ checker,
407
+ state.rootDir
408
+ ).pikkuFuncId
409
+ let packageName: string | undefined
410
+
411
+ if (
412
+ ts.isCallExpression(funcInitializer) &&
413
+ ts.isIdentifier(funcInitializer.expression) &&
414
+ funcInitializer.expression.text === 'ref'
415
+ ) {
416
+ const [firstArg] = funcInitializer.arguments
417
+ if (firstArg && ts.isStringLiteral(firstArg)) {
418
+ pikkuFuncId = firstArg.text
419
+ const addonNamespace = pikkuFuncId.includes(':')
420
+ ? pikkuFuncId.split(':')[0]
421
+ : null
422
+ packageName = addonNamespace
423
+ ? state.rpc.wireAddonDeclarations.get(addonNamespace)?.package
424
+ : undefined
425
+ }
426
+ } else if (ts.isIdentifier(funcInitializer)) {
427
+ packageName =
428
+ resolveAddonName(
429
+ funcInitializer,
430
+ checker,
431
+ state.rpc.wireAddonDeclarations
432
+ ) || undefined
433
+ }
434
+
435
+ return {
436
+ auth: getPropertyValue(obj, 'auth') as boolean | undefined,
437
+ contentType: getPropertyValue(obj, 'contentType') as string | undefined,
438
+ headers:
439
+ (getPropertyValue(obj, 'headers') as unknown as Record<string, string>) ||
440
+ undefined,
441
+ method,
442
+ route,
443
+ sse: getPropertyValue(obj, 'sse') as boolean | undefined,
444
+ tags: (getPropertyValue(obj, 'tags') as string[]) || undefined,
445
+ timeout: getPropertyValue(obj, 'timeout') as number | undefined,
446
+ func: {
447
+ pikkuFuncId,
448
+ ...(packageName && { packageName }),
449
+ },
450
+ }
451
+ }
452
+
211
453
  function processRoute(
212
454
  obj: ts.ObjectLiteralExpression,
213
455
  groupConfig: GroupConfig,