@pikku/inspector 0.6.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +3 -0
  3. package/dist/add-channel.d.ts +3 -0
  4. package/dist/add-channel.js +122 -0
  5. package/dist/add-file-extends-core-type.d.ts +3 -0
  6. package/dist/add-file-extends-core-type.js +38 -0
  7. package/dist/add-file-with-config.d.ts +3 -0
  8. package/dist/add-file-with-config.js +31 -0
  9. package/dist/add-file-with-factory.d.ts +3 -0
  10. package/dist/add-file-with-factory.js +48 -0
  11. package/dist/add-route.d.ts +4 -0
  12. package/dist/add-route.js +89 -0
  13. package/dist/add-schedule.d.ts +3 -0
  14. package/dist/add-schedule.js +32 -0
  15. package/dist/does-type-extend-core-type.d.ts +2 -0
  16. package/dist/does-type-extend-core-type.js +41 -0
  17. package/dist/get-property-value.d.ts +3 -0
  18. package/dist/get-property-value.js +60 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.js +1 -0
  21. package/dist/inspector.d.ts +3 -0
  22. package/dist/inspector.js +43 -0
  23. package/dist/types-map.d.ts +18 -0
  24. package/dist/types-map.js +103 -0
  25. package/dist/types.d.ts +49 -0
  26. package/dist/types.js +1 -0
  27. package/dist/utils.d.ts +30 -0
  28. package/dist/utils.js +245 -0
  29. package/dist/visit.d.ts +3 -0
  30. package/dist/visit.js +17 -0
  31. package/package.json +30 -0
  32. package/run-tests.sh +53 -0
  33. package/src/add-channel.ts +168 -0
  34. package/src/add-file-extends-core-type.ts +50 -0
  35. package/src/add-file-with-config.ts +45 -0
  36. package/src/add-file-with-factory.ts +65 -0
  37. package/src/add-route.ts +131 -0
  38. package/src/add-schedule.ts +47 -0
  39. package/src/does-type-extend-core-type.ts +53 -0
  40. package/src/get-property-value.ts +81 -0
  41. package/src/index.ts +4 -0
  42. package/src/inspector.ts +53 -0
  43. package/src/types-map.ts +130 -0
  44. package/src/types.ts +58 -0
  45. package/src/utils.ts +349 -0
  46. package/src/visit.ts +49 -0
  47. package/tsconfig.json +19 -0
  48. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,130 @@
1
+ export class TypesMap {
2
+ private map: Map<string, { originalName: string; path: string | null }> =
3
+ new Map()
4
+ public customTypes: Map<string, { type: string; references: string[] }> =
5
+ new Map()
6
+
7
+ public addCustomType(name: string, type: string, references: string[]) {
8
+ this.customTypes.set(name, { type, references })
9
+ }
10
+
11
+ public addType(originalName: string, path: string) {
12
+ this.map.set(originalName, { originalName, path })
13
+ }
14
+
15
+ public addUniqueType(originalName: string, path: string): string {
16
+ const uniqueName = `${originalName}_${Math.random().toString(36).substring(7)}`
17
+ this.map.set(uniqueName, { originalName, path })
18
+ return uniqueName
19
+ }
20
+
21
+ public getUniqueName(name: string): string {
22
+ const meta = this.getTypeMeta(name)
23
+ return meta.uniqueName
24
+ }
25
+
26
+ public getTypeMeta(name: string): {
27
+ originalName: string
28
+ uniqueName: string
29
+ path: string | null
30
+ } {
31
+ if (['string', 'number', 'boolean', 'null'].includes(name)) {
32
+ return {
33
+ originalName: name,
34
+ uniqueName: name,
35
+ path: null,
36
+ }
37
+ }
38
+
39
+ if (this.customTypes.has(name)) {
40
+ return {
41
+ originalName: name,
42
+ uniqueName: name,
43
+ path: null,
44
+ }
45
+ }
46
+
47
+ let meta = this.map.get(name)
48
+ if (!meta) {
49
+ meta = Array.from(this.map.entries()).find(
50
+ ([_, { originalName }]) => originalName === name
51
+ )?.[1]
52
+ }
53
+ if (!meta) {
54
+ throw new Error(`Type ${name} not found in typesMap`)
55
+ }
56
+
57
+ const getName = this.squash()
58
+ return {
59
+ uniqueName: getName(name),
60
+ originalName: meta.originalName,
61
+ path: meta?.path,
62
+ }
63
+ }
64
+
65
+ public exists(originalName: string, path: string): string | undefined {
66
+ const found = Array.from(this.map.entries()).find(([_, type]) => {
67
+ return type.path === path && type.originalName === originalName
68
+ })
69
+ return found ? found[0] : undefined
70
+ }
71
+
72
+ private squash() {
73
+ const duplicateNames = new Set<string>()
74
+ const pathToNamesMap = new Map<string, Map<string, string>>()
75
+ const nameOccurrences = new Map<string, Set<string>>()
76
+
77
+ // First pass: Track occurrences of each original name across paths
78
+ this.map.forEach(({ path, originalName }) => {
79
+ if (path) {
80
+ if (!nameOccurrences.has(originalName)) {
81
+ nameOccurrences.set(originalName, new Set())
82
+ }
83
+ nameOccurrences.get(originalName)!.add(path)
84
+ }
85
+ })
86
+
87
+ // Second pass: Populate pathToNamesMap
88
+ this.map.forEach(({ path, originalName }, uniqueName) => {
89
+ if (!path) return
90
+
91
+ if (!pathToNamesMap.has(path)) {
92
+ pathToNamesMap.set(path, new Map())
93
+ }
94
+
95
+ const isDuplicate = nameOccurrences.get(originalName)!.size > 1
96
+ if (isDuplicate) {
97
+ duplicateNames.add(uniqueName)
98
+ }
99
+ // Use uniqueName only if the originalName is duplicated across files
100
+ const nameToUse = isDuplicate ? uniqueName : originalName
101
+ pathToNamesMap.get(path)!.set(nameToUse, originalName)
102
+ })
103
+
104
+ const getName = (uniqueName: string) => {
105
+ if (duplicateNames.has(uniqueName)) {
106
+ return uniqueName
107
+ }
108
+ if (
109
+ uniqueName === 'string' ||
110
+ uniqueName === 'number' ||
111
+ uniqueName === 'boolean' ||
112
+ uniqueName === 'null'
113
+ ) {
114
+ return uniqueName
115
+ }
116
+ if (!this.map.has(uniqueName)) {
117
+ const found = Array.from(this.map.entries()).find(
118
+ ([_, { originalName }]) => originalName === uniqueName
119
+ )?.[1]
120
+ if (!found) {
121
+ throw new Error(`Type ${uniqueName} not found in typesMap`)
122
+ }
123
+ return found.originalName
124
+ }
125
+ return this.map.get(uniqueName)!.originalName
126
+ }
127
+
128
+ return getName
129
+ }
130
+ }
package/src/types.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { ChannelsMeta } from '@pikku/core/channel'
2
+ import { HTTPRoutesMeta } from '@pikku/core/http'
3
+ import { ScheduledTasksMeta } from '@pikku/core/scheduler'
4
+ import { TypesMap } from './types-map.js'
5
+
6
+ export type PathToNameAndType = Map<
7
+ string,
8
+ { variable: string; type: string | null; typePath: string | null }[]
9
+ >
10
+
11
+ export type MetaInputTypes = Map<
12
+ string,
13
+ {
14
+ query: string[] | undefined
15
+ params: string[] | undefined
16
+ body: string[] | undefined
17
+ }
18
+ >
19
+
20
+ export type APIFunctionMeta = Array<{
21
+ name: string
22
+ input: string
23
+ output: string
24
+ file: string
25
+ }>
26
+
27
+ export type InspectorAPIFunction = {
28
+ typesMap: TypesMap
29
+ meta: APIFunctionMeta
30
+ }
31
+
32
+ export interface InspectorHTTPState {
33
+ typesMap: TypesMap
34
+ metaInputTypes: MetaInputTypes
35
+ meta: HTTPRoutesMeta
36
+ files: Set<string>
37
+ }
38
+
39
+ export interface InspectorChannelState {
40
+ typesMap: TypesMap
41
+ metaInputTypes: MetaInputTypes
42
+ meta: ChannelsMeta
43
+ files: Set<string>
44
+ }
45
+
46
+ export interface InspectorState {
47
+ sessionServicesTypeImportMap: PathToNameAndType
48
+ userSessionTypeImportMap: PathToNameAndType
49
+ singletonServicesFactories: PathToNameAndType
50
+ sessionServicesFactories: PathToNameAndType
51
+ configFactories: PathToNameAndType
52
+ http: InspectorHTTPState
53
+ channels: InspectorChannelState
54
+ scheduledTasks: {
55
+ meta: ScheduledTasksMeta
56
+ files: Set<string>
57
+ }
58
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,349 @@
1
+ import * as ts from 'typescript'
2
+ import { TypesMap } from './types-map.js'
3
+
4
+ type FunctionTypes = {
5
+ inputTypes: ts.Type[]
6
+ inputs: null | string[]
7
+ outputTypes: ts.Type[]
8
+ outputs: null | string[]
9
+ }
10
+
11
+ export const extractTypeKeys = (type: ts.Type): string[] => {
12
+ return type.getProperties().map((symbol) => symbol.getName())
13
+ }
14
+
15
+ export 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 isValidVariableName = (name: string) => {
28
+ const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
29
+ return regex.test(name)
30
+ }
31
+
32
+ export const getNamesAndTypes = (
33
+ checker: ts.TypeChecker,
34
+ typesMap: TypesMap,
35
+ direction: 'Input' | 'Output',
36
+ funcName: string,
37
+ type: ts.Type
38
+ ) => {
39
+ const result: {
40
+ names: Set<string>
41
+ types: ts.Type[]
42
+ } = {
43
+ names: new Set(),
44
+ types: [],
45
+ }
46
+
47
+ const { names, types } = resolveUnionTypes(checker, type)
48
+ const firstName = names[0]
49
+ if (names.length > 1 || (firstName && !isValidVariableName(firstName))) {
50
+ const aliasType = names.join(' | ')
51
+ const aliasName = `${funcName.charAt(0).toUpperCase()}${funcName.slice(1)}${direction}`
52
+
53
+ result.names = new Set([aliasName])
54
+ result.types = types
55
+
56
+ const references = types
57
+ .map((t) => resolveTypeImports(t, typesMap, true))
58
+ .flat()
59
+ typesMap.addCustomType(aliasName, aliasType, references)
60
+ } else {
61
+ const uniqueNames = names
62
+ .map((name, i) => {
63
+ const type = types[i]
64
+ if (!type) {
65
+ throw new Error('TODO: Expected a type here to match name')
66
+ }
67
+ if (isPrimitiveType(type)) {
68
+ return name
69
+ }
70
+ return resolveTypeImports(type, typesMap, false)
71
+ })
72
+ .flat()
73
+ result.names = new Set(uniqueNames)
74
+ result.types = types
75
+ }
76
+
77
+ return {
78
+ names: Array.from(result.names),
79
+ types: result.types,
80
+ }
81
+ }
82
+
83
+ export const isPrimitiveType = (type: ts.Type): boolean => {
84
+ const primitiveFlags =
85
+ ts.TypeFlags.Number |
86
+ ts.TypeFlags.String |
87
+ ts.TypeFlags.Boolean |
88
+ ts.TypeFlags.BigInt |
89
+ ts.TypeFlags.ESSymbol |
90
+ ts.TypeFlags.Void |
91
+ ts.TypeFlags.Undefined |
92
+ ts.TypeFlags.Null |
93
+ ts.TypeFlags.Any |
94
+ ts.TypeFlags.Unknown
95
+
96
+ return (type.flags & primitiveFlags) !== 0
97
+ }
98
+
99
+ export const resolveUnionTypes = (
100
+ checker: ts.TypeChecker,
101
+ type: ts.Type
102
+ ): { types: ts.Type[]; names: string[] } => {
103
+ const types: ts.Type[] = []
104
+ const names: string[] = []
105
+
106
+ // Check if it's a union type AND not part of an intersection
107
+ if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
108
+ for (const t of type.types) {
109
+ const name = nullifyTypes(checker.typeToString(t))
110
+ if (name) {
111
+ types.push(t)
112
+ names.push(name)
113
+ }
114
+ }
115
+ } else {
116
+ const name = nullifyTypes(checker.typeToString(type))
117
+ if (name) {
118
+ types.push(type)
119
+ names.push(name)
120
+ }
121
+ }
122
+
123
+ return { types, names }
124
+ }
125
+
126
+ export const resolveTypeImports = (
127
+ type: ts.Type,
128
+ resolvedTypes: TypesMap,
129
+ isCustom: boolean
130
+ ): string[] => {
131
+ const types: string[] = []
132
+
133
+ const visitType = (currentType: ts.Type) => {
134
+ const symbol = currentType.aliasSymbol || currentType.getSymbol()
135
+
136
+ if (symbol) {
137
+ const declarations = symbol.getDeclarations()
138
+ const declaration = declarations?.[0]
139
+ if (declaration) {
140
+ const sourceFile = declaration.getSourceFile()
141
+ const path = sourceFile.fileName
142
+
143
+ // Skip built-in utility types or TypeScript lib types
144
+ if (
145
+ !path.includes('node_modules/typescript') &&
146
+ symbol.getName() !== '__type' &&
147
+ !isPrimitiveType(currentType)
148
+ ) {
149
+ const originalName = symbol.getName()
150
+ // Check if the type is already in the map
151
+ let uniqueName = resolvedTypes.exists(originalName, path)
152
+ if (!uniqueName) {
153
+ if (isCustom) {
154
+ uniqueName = resolvedTypes.addUniqueType(originalName, path)
155
+ } else {
156
+ resolvedTypes.addType(originalName, path)
157
+ uniqueName = originalName
158
+ }
159
+ }
160
+ types.push(uniqueName)
161
+ }
162
+ }
163
+ }
164
+
165
+ if (isCustom) {
166
+ // Handle nested utility types like Partial, Pick, etc.
167
+ if (currentType.aliasTypeArguments) {
168
+ currentType.aliasTypeArguments.forEach(visitType)
169
+ }
170
+
171
+ // Handle intersections and unions
172
+ if (currentType.isUnionOrIntersection()) {
173
+ currentType.types.forEach(visitType)
174
+ }
175
+
176
+ // Handle object types with type arguments
177
+ if (
178
+ currentType.flags & ts.TypeFlags.Object &&
179
+ (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
180
+ ) {
181
+ const typeRef = currentType as ts.TypeReference
182
+ typeRef.typeArguments?.forEach(visitType)
183
+ }
184
+ }
185
+ }
186
+
187
+ visitType(type)
188
+ return types
189
+ }
190
+
191
+ export const getPropertyAssignment = (
192
+ obj: ts.ObjectLiteralExpression,
193
+ name: string
194
+ ) => {
195
+ const property = obj.properties.find(
196
+ (p) =>
197
+ (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
198
+ ts.isIdentifier(p.name) &&
199
+ p.name.text === name
200
+ )
201
+ if (!property) {
202
+ console.error(`Missing property '${name}' in object`)
203
+ return null
204
+ }
205
+ return property
206
+ }
207
+
208
+ export const getTypeArgumentsOfType = (
209
+ checker: ts.TypeChecker,
210
+ type: ts.Type
211
+ ): readonly ts.Type[] | null => {
212
+ if (type.isUnionOrIntersection()) {
213
+ const types: ts.Type[] = []
214
+ for (const subType of type.types) {
215
+ const subTypeArgs = getTypeArgumentsOfType(checker, subType)
216
+ if (subTypeArgs) {
217
+ types.push(...subTypeArgs)
218
+ }
219
+ }
220
+ return types.length > 0 ? types : null
221
+ }
222
+
223
+ // If the type is a TypeReference with typeArguments, return them
224
+ if (
225
+ type.flags & ts.TypeFlags.Object &&
226
+ (type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
227
+ ) {
228
+ const typeRef = type as ts.TypeReference
229
+ if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
230
+ return typeRef.typeArguments
231
+ }
232
+ }
233
+
234
+ // If the type is an alias with aliasTypeArguments, return them
235
+ if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
236
+ return type.aliasTypeArguments as ts.Type[]
237
+ }
238
+
239
+ return null
240
+ }
241
+
242
+ export const getFunctionTypes = (
243
+ checker: ts.TypeChecker,
244
+ obj: ts.ObjectLiteralExpression,
245
+ {
246
+ typesMap,
247
+ funcName,
248
+ subFunctionName = funcName,
249
+ inputIndex,
250
+ outputIndex,
251
+ }: {
252
+ typesMap: TypesMap
253
+ subFunctionName?: string
254
+ funcName: string
255
+ inputIndex: number
256
+ outputIndex: number
257
+ }
258
+ ): FunctionTypes => {
259
+ const result: FunctionTypes = {
260
+ inputTypes: [],
261
+ inputs: null,
262
+ outputTypes: [],
263
+ outputs: null,
264
+ }
265
+
266
+ const property = getPropertyAssignment(obj, subFunctionName)
267
+ if (!property) {
268
+ return result
269
+ }
270
+
271
+ let type: ts.Type | undefined
272
+
273
+ // Handle shorthand property assignment
274
+ if (ts.isShorthandPropertyAssignment(property)) {
275
+ const symbol = checker.getShorthandAssignmentValueSymbol(property)
276
+ if (symbol) {
277
+ type = checker.getTypeOfSymbolAtLocation(symbol, property)
278
+ if (funcName === 'func') {
279
+ funcName = symbol.name
280
+ }
281
+ }
282
+ }
283
+ // Handle regular property assignment
284
+ else if (ts.isPropertyAssignment(property)) {
285
+ if (ts.isObjectLiteralExpression(property.initializer)) {
286
+ return getFunctionTypes(checker, property.initializer, {
287
+ typesMap,
288
+ funcName,
289
+ subFunctionName: 'func',
290
+ inputIndex,
291
+ outputIndex,
292
+ })
293
+ }
294
+
295
+ if (property.initializer) {
296
+ type = checker.getTypeAtLocation(property.initializer)
297
+ if (funcName === 'func') {
298
+ funcName = property.initializer.getText()
299
+ }
300
+ }
301
+ }
302
+
303
+ if (!type) {
304
+ console.error(`Unable to resolve type for property '${funcName}'`)
305
+ return result
306
+ }
307
+
308
+ // Access type arguments from TypeReference
309
+ const typeArguments = getTypeArgumentsOfType(checker, type)
310
+
311
+ if (!typeArguments || typeArguments.length === 0) {
312
+ // This is the case for inline functions. In this case we would want to
313
+ // get the types from the second argument of the function...
314
+ console.error(
315
+ `\x1b[31m• No generic type arguments found for ${funcName}. Support for inline functions is not yet implemented.\x1b[0m`
316
+ )
317
+ return result
318
+ }
319
+
320
+ if (inputIndex !== undefined && inputIndex < typeArguments.length) {
321
+ const { names, types } = getNamesAndTypes(
322
+ checker,
323
+ typesMap,
324
+ 'Input',
325
+ funcName,
326
+ typeArguments[inputIndex]!
327
+ )
328
+ result.inputs = names
329
+ result.inputTypes = types
330
+ } else {
331
+ console.log(`No input defined for ${funcName}`)
332
+ }
333
+
334
+ if (outputIndex !== undefined && outputIndex < typeArguments.length) {
335
+ const { names, types } = getNamesAndTypes(
336
+ checker,
337
+ typesMap,
338
+ 'Output',
339
+ funcName,
340
+ typeArguments[outputIndex]!
341
+ )
342
+ result.outputs = names
343
+ result.outputTypes = types
344
+ } else {
345
+ console.info(`No output defined for ${funcName}`)
346
+ }
347
+
348
+ return result
349
+ }
package/src/visit.ts ADDED
@@ -0,0 +1,49 @@
1
+ import * as ts from 'typescript'
2
+ import { addFileWithFactory } from './add-file-with-factory.js'
3
+ import { addFileExtendsCoreType } from './add-file-extends-core-type.js'
4
+ import { addRoute } from './add-route.js'
5
+ import { addSchedule } from './add-schedule.js'
6
+ import { addChannel } from './add-channel.js'
7
+ import { InspectorState } from './types.js'
8
+
9
+ export const visit = (
10
+ checker: ts.TypeChecker,
11
+ node: ts.Node,
12
+ state: InspectorState
13
+ ) => {
14
+ addFileExtendsCoreType(
15
+ node,
16
+ checker,
17
+ state.sessionServicesTypeImportMap,
18
+ 'CoreServices'
19
+ )
20
+
21
+ addFileExtendsCoreType(
22
+ node,
23
+ checker,
24
+ state.userSessionTypeImportMap,
25
+ 'CoreUserSession'
26
+ )
27
+
28
+ addFileWithFactory(
29
+ node,
30
+ checker,
31
+ state.singletonServicesFactories,
32
+ 'CreateSingletonServices'
33
+ )
34
+
35
+ addFileWithFactory(
36
+ node,
37
+ checker,
38
+ state.sessionServicesFactories,
39
+ 'CreateSessionServices'
40
+ )
41
+
42
+ addFileWithFactory(node, checker, state.configFactories, 'CreateConfig')
43
+
44
+ addRoute(node, checker, state)
45
+ addSchedule(node, checker, state)
46
+ addChannel(node, checker, state)
47
+
48
+ ts.forEachChild(node, (child) => visit(checker, child, state))
49
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "module": "Node16",
6
+ "outDir": "dist",
7
+ "target": "esnext",
8
+ "declaration": true,
9
+ "resolveJsonModule": true,
10
+ "composite": true
11
+ },
12
+ "include": ["bin/**/*.ts", "src/**/*.ts"],
13
+ "exclude": ["**/*.test.ts", "node_modules", "bin/dist"],
14
+ "references": [
15
+ {
16
+ "path": "../core/tsconfig.json"
17
+ }
18
+ ]
19
+ }