@pikku/inspector 0.6.4 → 0.7.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.
@@ -0,0 +1,376 @@
1
+ import * as ts from 'typescript'
2
+ import { InspectorState, InspectorFilters } from './types.js'
3
+ import { TypesMap } from './types-map.js'
4
+ import {
5
+ extractFunctionName,
6
+ getPropertyAssignmentInitializer,
7
+ } from './utils.js'
8
+ import { FunctionServicesMeta } from '@pikku/core'
9
+
10
+ const isValidVariableName = (name: string) => {
11
+ const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
12
+ return regex.test(name)
13
+ }
14
+
15
+ const nullifyTypes = (type: string | null) => {
16
+ if (
17
+ type === 'void' ||
18
+ type === 'undefined' ||
19
+ type === 'unknown' ||
20
+ type === 'any'
21
+ ) {
22
+ return null
23
+ }
24
+ return type
25
+ }
26
+
27
+ const resolveTypeImports = (
28
+ type: ts.Type,
29
+ resolvedTypes: TypesMap,
30
+ isCustom: boolean
31
+ ): string[] => {
32
+ const types: string[] = []
33
+
34
+ const visitType = (currentType: ts.Type) => {
35
+ const symbol = currentType.aliasSymbol || currentType.getSymbol()
36
+
37
+ if (symbol) {
38
+ const declarations = symbol.getDeclarations()
39
+ const declaration = declarations?.[0]
40
+ if (declaration) {
41
+ const sourceFile = declaration.getSourceFile()
42
+ const path = sourceFile.fileName
43
+
44
+ // Skip built-in utility types or TypeScript lib types
45
+ if (
46
+ !path.includes('node_modules/typescript') &&
47
+ symbol.getName() !== '__type' &&
48
+ !isPrimitiveType(currentType)
49
+ ) {
50
+ const originalName = symbol.getName()
51
+ // Check if the type is already in the map
52
+ let uniqueName = resolvedTypes.exists(originalName, path)
53
+ if (!uniqueName) {
54
+ if (isCustom) {
55
+ uniqueName = resolvedTypes.addUniqueType(originalName, path)
56
+ } else {
57
+ resolvedTypes.addType(originalName, path)
58
+ uniqueName = originalName
59
+ }
60
+ }
61
+ types.push(uniqueName)
62
+ }
63
+ }
64
+ }
65
+
66
+ if (isCustom) {
67
+ // Handle nested utility types like Partial, Pick, etc.
68
+ if (currentType.aliasTypeArguments) {
69
+ currentType.aliasTypeArguments.forEach(visitType)
70
+ }
71
+
72
+ // Handle intersections and unions
73
+ if (currentType.isUnionOrIntersection()) {
74
+ currentType.types.forEach(visitType)
75
+ }
76
+
77
+ // Handle object types with type arguments
78
+ if (
79
+ currentType.flags & ts.TypeFlags.Object &&
80
+ (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
81
+ ) {
82
+ const typeRef = currentType as ts.TypeReference
83
+ typeRef.typeArguments?.forEach(visitType)
84
+ }
85
+ }
86
+ }
87
+
88
+ visitType(type)
89
+ return types
90
+ }
91
+
92
+ const resolveUnionTypes = (
93
+ checker: ts.TypeChecker,
94
+ type: ts.Type
95
+ ): { types: ts.Type[]; names: string[] } => {
96
+ const types: ts.Type[] = []
97
+ const names: string[] = []
98
+
99
+ // Check if it's a union type AND not part of an intersection
100
+ if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
101
+ for (const t of type.types) {
102
+ const name = nullifyTypes(checker.typeToString(t))
103
+ if (name) {
104
+ types.push(t)
105
+ names.push(name)
106
+ }
107
+ }
108
+ } else {
109
+ const name = nullifyTypes(checker.typeToString(type))
110
+ if (name) {
111
+ types.push(type)
112
+ names.push(name)
113
+ }
114
+ }
115
+
116
+ return { types, names }
117
+ }
118
+
119
+ const getNamesAndTypes = (
120
+ checker: ts.TypeChecker,
121
+ typesMap: TypesMap,
122
+ direction: 'Input' | 'Output',
123
+ funcName: string,
124
+ type?: ts.Type
125
+ ) => {
126
+ if (!type) {
127
+ return { names: [], types: [] }
128
+ }
129
+
130
+ // 1) Handle an explicit void (or undefined) type up front
131
+ if (type.flags & ts.TypeFlags.VoidLike) {
132
+ return {
133
+ names: ['void'],
134
+ types: [type],
135
+ }
136
+ }
137
+
138
+ // 2) For unions, resolve all member names/types
139
+ const { names: rawNames, types: rawTypes } = resolveUnionTypes(checker, type)
140
+
141
+ // If the union is exactly [void], we'd have caught it above.
142
+ // If it's e.g. [string, void], rawNames should already include 'void'.
143
+
144
+ // 3) If multiple names or the single name isn't a valid identifier,
145
+ // we emit an alias type.
146
+ const firstName = rawNames[0]
147
+ if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
148
+ const aliasType = rawNames.join(' | ')
149
+ const aliasName =
150
+ funcName.charAt(0).toUpperCase() + funcName.slice(1) + direction
151
+
152
+ // record the alias in your TypesMap
153
+ const references = rawTypes
154
+ .map((t) => resolveTypeImports(t, typesMap, true))
155
+ .flat()
156
+
157
+ typesMap.addCustomType(aliasName, aliasType, references)
158
+
159
+ return {
160
+ names: [aliasName],
161
+ types: rawTypes,
162
+ }
163
+ }
164
+
165
+ // 4) Single, valid name → inline it
166
+ const uniqueNames = rawNames
167
+ .map((name, i) => {
168
+ const t = rawTypes[i]
169
+ if (!t) {
170
+ throw new Error(`Expected type for name "${name}" in ${funcName}`)
171
+ }
172
+ if (isPrimitiveType(t)) {
173
+ return name
174
+ }
175
+ // non-primitive: import/alias it inline
176
+ return resolveTypeImports(t, typesMap, false)
177
+ })
178
+ .flat()
179
+
180
+ return {
181
+ names: uniqueNames,
182
+ types: rawTypes,
183
+ }
184
+ }
185
+
186
+ const isPrimitiveType = (type: ts.Type): boolean => {
187
+ const primitiveFlags =
188
+ ts.TypeFlags.Number |
189
+ ts.TypeFlags.String |
190
+ ts.TypeFlags.Boolean |
191
+ ts.TypeFlags.BigInt |
192
+ ts.TypeFlags.ESSymbol |
193
+ ts.TypeFlags.Void |
194
+ ts.TypeFlags.Undefined |
195
+ ts.TypeFlags.Null |
196
+ ts.TypeFlags.Any |
197
+ ts.TypeFlags.Unknown |
198
+ ts.TypeFlags.VoidLike
199
+
200
+ return (type.flags & primitiveFlags) !== 0
201
+ }
202
+
203
+ /**
204
+ * If `type` is a `Promise<T>`, return `T`, otherwise return `type` itself.
205
+ */
206
+ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
207
+ if (!type?.symbol) return type
208
+
209
+ const isPromise =
210
+ type.symbol.name === 'Promise' &&
211
+ checker.getFullyQualifiedName(type.symbol).includes('Promise')
212
+
213
+ // aliasTypeArguments covers most Promise<T> cases
214
+ if (isPromise && type.aliasTypeArguments?.length === 1) {
215
+ return type.aliasTypeArguments[0]!
216
+ }
217
+
218
+ // fallback for raw TypeReference
219
+ if (isPromise && (type as ts.TypeReference).typeArguments?.length === 1) {
220
+ return (type as ts.TypeReference).typeArguments![0]!
221
+ }
222
+
223
+ return type
224
+ }
225
+
226
+ /**
227
+ * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
228
+ * then push into state.functions.meta.
229
+ */
230
+ export function addFunctions(
231
+ node: ts.Node,
232
+ checker: ts.TypeChecker,
233
+ state: InspectorState,
234
+ filters: InspectorFilters
235
+ ) {
236
+ if (!ts.isCallExpression(node)) return
237
+
238
+ const { expression, arguments: args, typeArguments } = node
239
+
240
+ // only handle calls like pikkuFunc(...)
241
+ if (!ts.isIdentifier(expression)) {
242
+ return
243
+ }
244
+
245
+ // Match identifiers that contain both "pikku" and "func" (case insensitive)
246
+ const pikkuFuncPattern = /pikku.*func/i
247
+ if (!pikkuFuncPattern.test(expression.text)) {
248
+ return
249
+ }
250
+
251
+ // only handle calls like pikkuFunc(...)
252
+ if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
253
+ return
254
+ }
255
+
256
+ if (args.length === 0) return
257
+
258
+ const { pikkuFuncName, name } = extractFunctionName(node, checker)
259
+
260
+ // determine the actual handler expression:
261
+ // either the `func` prop or the first argument directly
262
+ let handlerNode: ts.Expression = args[0]!
263
+ if (ts.isObjectLiteralExpression(handlerNode)) {
264
+ const fnProp = getPropertyAssignmentInitializer(
265
+ handlerNode,
266
+ 'func',
267
+ true,
268
+ checker
269
+ )
270
+ if (
271
+ !fnProp ||
272
+ (!ts.isArrowFunction(fnProp) && !ts.isFunctionExpression(fnProp))
273
+ ) {
274
+ console.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
275
+ return
276
+ }
277
+ handlerNode = fnProp
278
+ }
279
+
280
+ if (
281
+ !ts.isArrowFunction(handlerNode) &&
282
+ !ts.isFunctionExpression(handlerNode)
283
+ ) {
284
+ console.error(`• Handler for ${name} is not a function.`)
285
+ return
286
+ }
287
+
288
+ const services: FunctionServicesMeta = {
289
+ optimized: true,
290
+ services: [],
291
+ }
292
+
293
+ const firstParam = handlerNode.parameters[0]
294
+ if (firstParam) {
295
+ if (ts.isObjectBindingPattern(firstParam.name)) {
296
+ for (const elem of firstParam.name.elements) {
297
+ const original =
298
+ elem.propertyName && ts.isIdentifier(elem.propertyName)
299
+ ? elem.propertyName.text
300
+ : ts.isIdentifier(elem.name)
301
+ ? elem.name.text
302
+ : undefined
303
+ if (original) {
304
+ services.services.push(original)
305
+ }
306
+ }
307
+ } else if (
308
+ ts.isIdentifier(firstParam.name) &&
309
+ !firstParam.name.text.startsWith('_')
310
+ ) {
311
+ services.optimized = false
312
+ }
313
+ }
314
+
315
+ // --- Generics → ts.Type[], unwrapped from Promise ---
316
+ const genericTypes: ts.Type[] = (typeArguments ?? [])
317
+ .map((tn) => checker.getTypeFromTypeNode(tn))
318
+ .map((t) => unwrapPromise(checker, t))
319
+
320
+ // --- Input Extraction ---
321
+ let { names: inputNames, types: inputTypes } = getNamesAndTypes(
322
+ checker,
323
+ state.functions.typesMap,
324
+ 'Input',
325
+ name,
326
+ genericTypes[0]
327
+ )
328
+ if (inputTypes.length === 0) {
329
+ console.warn(
330
+ `\x1b[31m• Unknown input type for '${name}', assuming void.\x1b[0m`
331
+ )
332
+ }
333
+
334
+ // --- Output Extraction ---
335
+ let outputNames: string[] = []
336
+ if (genericTypes.length >= 2) {
337
+ outputNames = getNamesAndTypes(
338
+ checker,
339
+ state.functions.typesMap,
340
+ 'Output',
341
+ name,
342
+ genericTypes[1]
343
+ ).names
344
+ } else {
345
+ const sig = checker.getSignatureFromDeclaration(handlerNode)
346
+ if (sig) {
347
+ const rawRet = checker.getReturnTypeOfSignature(sig)
348
+ const unwrapped = unwrapPromise(checker, rawRet)
349
+ outputNames = getNamesAndTypes(
350
+ checker,
351
+ state.functions.typesMap,
352
+ 'Output',
353
+ pikkuFuncName,
354
+ unwrapped
355
+ ).names
356
+ }
357
+ }
358
+
359
+ // --- Record metadata ---
360
+ state.functions.files.add(node.getSourceFile().fileName)
361
+
362
+ if (inputNames.length > 1) {
363
+ console.warn(
364
+ 'More than one input type detected, only the first one will be used as a schema.'
365
+ )
366
+ }
367
+
368
+ state.functions.meta[pikkuFuncName] = {
369
+ pikkuFuncName,
370
+ name,
371
+ services,
372
+ schemaName: inputNames[0] ?? null,
373
+ inputs: inputNames.filter((n) => n !== 'void') ?? null,
374
+ outputs: outputNames.filter((n) => n !== 'void') ?? null,
375
+ }
376
+ }
@@ -3,136 +3,121 @@ import { getPropertyValue } from './get-property-value.js'
3
3
  import { pathToRegexp } from 'path-to-regexp'
4
4
  import { HTTPMethod } from '@pikku/core/http'
5
5
  import { APIDocs } from '@pikku/core'
6
- import { extractTypeKeys, getFunctionTypes, matchesFilters } from './utils.js'
7
- import { MetaInputTypes, InspectorState, InspectorFilters } from './types.js'
8
-
6
+ import {
7
+ extractFunctionName,
8
+ getPropertyAssignmentInitializer,
9
+ matchesFilters,
10
+ } from './utils.js'
11
+ import { InspectorState, InspectorFilters } from './types.js'
12
+
13
+ /**
14
+ * Populate metaInputTypes for a given route based on method, input type,
15
+ * query and params. Returns undefined (we only mutate metaTypes).
16
+ */
9
17
  export const getInputTypes = (
10
- metaTypes: MetaInputTypes,
18
+ metaTypes: Map<
19
+ string,
20
+ { query?: string[]; params?: string[]; body?: string[] }
21
+ >,
11
22
  methodType: string,
12
23
  inputType: string | null,
13
24
  queryValues: string[],
14
25
  paramsValues: string[]
15
- ) => {
16
- if (!inputType) {
17
- return undefined
18
- }
19
-
20
- if (inputType) {
21
- metaTypes.set(inputType, {
22
- query: queryValues,
23
- params: paramsValues,
24
- body: ['post', 'put', 'patch'].includes(methodType)
25
- ? [...new Set([...queryValues, ...paramsValues])]
26
- : [],
27
- })
28
- }
29
-
30
- return undefined
26
+ ): undefined => {
27
+ if (!inputType) return
28
+ metaTypes.set(inputType, {
29
+ query: queryValues,
30
+ params: paramsValues,
31
+ body: ['post', 'put', 'patch'].includes(methodType)
32
+ ? [...new Set([...queryValues, ...paramsValues])]
33
+ : [],
34
+ })
35
+ return
31
36
  }
32
37
 
33
- export const addRoute = (
38
+ /**
39
+ * Simplified addHTTPRoute: re-uses function metadata from state.functions.meta
40
+ * instead of re-inferring types here.
41
+ */
42
+ export const addHTTPRoute = (
34
43
  node: ts.Node,
35
44
  checker: ts.TypeChecker,
36
45
  state: InspectorState,
37
46
  filters: InspectorFilters
38
47
  ) => {
39
- if (!ts.isCallExpression(node)) {
40
- return
41
- }
48
+ // only look at calls
49
+ if (!ts.isCallExpression(node)) return
50
+
51
+ const { expression, arguments: args } = node
52
+ if (!ts.isIdentifier(expression) || expression.text !== 'addHTTPRoute') return
42
53
 
43
- const args = node.arguments
54
+ // must pass an object literal
44
55
  const firstArg = args[0]
45
- const expression = node.expression
56
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
57
+ const obj = firstArg
58
+
59
+ // --- extract HTTP metadata ---
60
+ const route = getPropertyValue(obj, 'route') as string | null
61
+ if (!route) return
62
+
63
+ const keys = pathToRegexp(route).keys
64
+ const params = keys.filter((k) => k.type === 'param').map((k) => k.name)
65
+
66
+ const method =
67
+ (getPropertyValue(obj, 'method') as string)?.toLowerCase() || 'get'
68
+ const docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
69
+ const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
70
+ const query = (getPropertyValue(obj, 'query') as string[]) || []
46
71
 
47
- // Check if the call is to addRoute
48
- if (!ts.isIdentifier(expression) || expression.text !== 'addRoute') {
72
+ if (!matchesFilters(filters, { tags }, { type: 'http', name: route })) {
49
73
  return
50
74
  }
51
75
 
52
- if (!firstArg) {
76
+ // --- find the referenced function ---
77
+ const funcInitializer = getPropertyAssignmentInitializer(
78
+ obj,
79
+ 'func',
80
+ true,
81
+ checker
82
+ )
83
+ if (!funcInitializer) {
84
+ console.error(`• No valid 'func' property for route '${route}'.`)
53
85
  return
54
86
  }
55
87
 
56
- let docs: APIDocs | undefined
57
- let methodValue: string | null = null
58
- let paramsValues: string[] | null = []
59
- let queryValues: string[] | [] = []
60
- let tags: string[] | [] = []
61
- let routeValue: string | null = null
88
+ const funcName = extractFunctionName(funcInitializer, checker).pikkuFuncName
62
89
 
63
- // Check if the first argument is an object literal
64
- if (ts.isObjectLiteralExpression(firstArg)) {
65
- const obj = firstArg
66
-
67
- routeValue = getPropertyValue(obj, 'route') as string | null
68
- if (!routeValue) {
69
- return
70
- }
71
-
72
- const { keys } = pathToRegexp(routeValue)
73
- paramsValues = keys.reduce((result, { type, name }) => {
74
- if (type === 'param') {
75
- result.push(name)
76
- }
77
- return result
78
- }, [] as string[])
79
-
80
- docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
81
- methodValue = getPropertyValue(obj, 'method') as string
82
- queryValues = (getPropertyValue(obj, 'query') as string[]) || []
83
- tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
84
-
85
- if (
86
- !matchesFilters(filters, { tags }, { type: 'http', name: routeValue })
87
- ) {
88
- return
89
- }
90
-
91
- let { inputs, outputs, inputTypes } = getFunctionTypes(checker, obj, {
92
- funcName: 'func',
93
- inputIndex: 0,
94
- outputIndex: 1,
95
- typesMap: state.http.typesMap,
96
- })
97
-
98
- const input = inputs ? inputs[0] || null : null
99
- const output = outputs ? outputs[0] || null : null
100
-
101
- if (inputs && inputs?.length > 1) {
102
- console.error(
103
- `Only one input type is currently allowed for method '${methodValue}' and route '${routeValue}': \n\t${inputs.join('\n\t')}`
104
- )
105
- }
106
-
107
- if (outputs && outputs?.length > 1) {
108
- console.error(
109
- `Only one output type is currently allowed for method '${methodValue}' and route '${routeValue}': \n\t${outputs.join('\n\t')}`
110
- )
111
- }
112
-
113
- if (inputTypes[0] && !['post', 'put', 'patch'].includes(methodValue)) {
114
- queryValues = [
115
- ...new Set([...queryValues, ...extractTypeKeys(inputTypes[0])]),
116
- ].filter((query) => !paramsValues?.includes(query))
117
- }
118
-
119
- state.http.files.add(node.getSourceFile().fileName)
120
- state.http.meta.push({
121
- route: routeValue,
122
- method: methodValue! as HTTPMethod,
123
- input,
124
- output,
125
- params: paramsValues.length > 0 ? paramsValues : undefined,
126
- query: queryValues.length > 0 ? queryValues : undefined,
127
- inputTypes: getInputTypes(
128
- state.http.metaInputTypes,
129
- methodValue,
130
- input,
131
- queryValues,
132
- paramsValues
133
- ),
134
- docs,
135
- tags,
136
- })
90
+ // lookup existing function metadata
91
+ const fnMeta = state.functions.meta[funcName]
92
+ if (!fnMeta) {
93
+ console.log(Object.keys(state.functions.meta))
94
+ console.error(`• No function metadata found for '${funcName}'.`)
95
+ return
137
96
  }
97
+ const input = fnMeta.inputs?.[0] || null
98
+ const output = fnMeta.outputs?.[0] || null
99
+
100
+ // --- compute inputTypes (body/query/params) ---
101
+ const inputTypes = getInputTypes(
102
+ state.http.metaInputTypes,
103
+ method,
104
+ input,
105
+ query,
106
+ params
107
+ )
108
+
109
+ // --- record route ---
110
+ state.http.files.add(node.getSourceFile().fileName)
111
+ state.http.meta.push({
112
+ pikkuFuncName: funcName,
113
+ route,
114
+ method: method as HTTPMethod,
115
+ input,
116
+ output,
117
+ params: params.length > 0 ? params : undefined,
118
+ query: query.length > 0 ? query : undefined,
119
+ inputTypes,
120
+ docs,
121
+ tags,
122
+ })
138
123
  }
@@ -2,11 +2,11 @@ import * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
3
  import { APIDocs } from '@pikku/core'
4
4
  import { InspectorFilters, InspectorState } from './types.js'
5
- import { matchesFilters } from './utils.js'
5
+ import { extractFunctionName, matchesFilters } from './utils.js'
6
6
 
7
7
  export const addSchedule = (
8
8
  node: ts.Node,
9
- _checker: ts.TypeChecker,
9
+ checker: ts.TypeChecker,
10
10
  state: InspectorState,
11
11
  filters: InspectorFilters
12
12
  ) => {
@@ -35,6 +35,25 @@ export const addSchedule = (
35
35
  const docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
36
36
  const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
37
37
 
38
+ // --- find the referenced function ---
39
+ const funcProp = obj.properties.find(
40
+ (p) =>
41
+ ts.isPropertyAssignment(p) &&
42
+ ts.isIdentifier(p.name) &&
43
+ p.name.text === 'func'
44
+ ) as ts.PropertyAssignment | undefined
45
+
46
+ if (!funcProp || !ts.isIdentifier(funcProp.initializer)) {
47
+ console.error(
48
+ `• No valid 'func' property for scheduled task '${nameValue}'.`
49
+ )
50
+ return
51
+ }
52
+ const pikkuFuncName = extractFunctionName(
53
+ funcProp.initializer,
54
+ checker
55
+ ).pikkuFuncName
56
+
38
57
  if (!nameValue || !scheduleValue) {
39
58
  return
40
59
  }
@@ -46,11 +65,12 @@ export const addSchedule = (
46
65
  }
47
66
 
48
67
  state.scheduledTasks.files.add(node.getSourceFile().fileName)
49
- state.scheduledTasks.meta.push({
68
+ state.scheduledTasks.meta[nameValue] = {
69
+ pikkuFuncName,
50
70
  name: nameValue,
51
71
  schedule: scheduleValue,
52
72
  docs,
53
73
  tags,
54
- })
74
+ }
55
75
  }
56
76
  }
package/src/inspector.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript'
2
- import { visit } from './visit.js'
2
+ import { visitSetup, visitRoutes } from './visit.js'
3
3
  import { TypesMap } from './types-map.js'
4
4
  import {
5
5
  InspectorState,
@@ -31,6 +31,11 @@ export const inspect = (
31
31
  singletonServicesFactories: new Map(),
32
32
  sessionServicesFactories: new Map(),
33
33
  configFactories: new Map(),
34
+ functions: {
35
+ typesMap: new TypesMap(),
36
+ meta: {},
37
+ files: new Set(),
38
+ },
34
39
  http: {
35
40
  typesMap: new TypesMap(),
36
41
  metaInputTypes: new Map(),
@@ -41,22 +46,29 @@ export const inspect = (
41
46
  typesMap: new TypesMap(),
42
47
  metaInputTypes: new Map(),
43
48
  files: new Set(),
44
- meta: [],
49
+ meta: {},
45
50
  },
46
51
  scheduledTasks: {
47
- meta: [],
52
+ meta: {},
48
53
  files: new Set(),
49
54
  },
50
55
  }
51
56
 
57
+ // First sweep: add all functions
52
58
  for (const sourceFile of sourceFiles) {
53
59
  ts.forEachChild(sourceFile, (child) =>
54
- visit(checker, child, state, filters)
60
+ visitSetup(checker, child, state, filters)
55
61
  )
56
62
  }
57
63
 
58
- // Normalise the typesMap
64
+ // Second sweep: add all transports
65
+ for (const sourceFile of sourceFiles) {
66
+ ts.forEachChild(sourceFile, (child) =>
67
+ visitRoutes(checker, child, state, filters)
68
+ )
69
+ }
59
70
 
71
+ // Normalise the typesMap
60
72
  state.http = normalizeHTTPTypes(state.http)
61
73
 
62
74
  return state