@pikku/inspector 0.6.4 → 0.7.0

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,84 +0,0 @@
1
- import { APIDocs } from '@pikku/core'
2
- import * as ts from 'typescript'
3
-
4
- export const getPropertyValue = (
5
- obj: ts.ObjectLiteralExpression,
6
- propertyName: string
7
- ): string | string[] | null | APIDocs => {
8
- const property = obj.properties.find(
9
- (p) =>
10
- ts.isPropertyAssignment(p) &&
11
- ts.isIdentifier(p.name) &&
12
- p.name.text === propertyName
13
- )
14
-
15
- if (property && ts.isPropertyAssignment(property)) {
16
- const initializer = property.initializer
17
-
18
- // Special handling for 'query' -> expect an array of strings
19
- if (
20
- ['query', 'tags'].includes(propertyName) &&
21
- ts.isArrayLiteralExpression(initializer)
22
- ) {
23
- const stringArray = initializer.elements
24
- .map((element) => {
25
- if (ts.isStringLiteral(element)) {
26
- return element.text
27
- }
28
- return null
29
- })
30
- .filter((item) => item !== null) as string[] // Filter non-null and assert type
31
-
32
- return stringArray.length > 0 ? stringArray : null
33
- }
34
-
35
- // Special handling for 'docs' -> expect RouteDocs
36
- if (propertyName === 'docs' && ts.isObjectLiteralExpression(initializer)) {
37
- const docs: APIDocs = {}
38
-
39
- initializer.properties.forEach((prop) => {
40
- if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
41
- const propName = prop.name.text
42
-
43
- if (propName === 'summary' && ts.isStringLiteral(prop.initializer)) {
44
- docs.summary = prop.initializer.text
45
- } else if (
46
- propName === 'description' &&
47
- ts.isStringLiteral(prop.initializer)
48
- ) {
49
- docs.description = prop.initializer.text
50
- } else if (
51
- propName === 'tags' &&
52
- ts.isArrayLiteralExpression(prop.initializer)
53
- ) {
54
- docs.tags = prop.initializer.elements
55
- .filter(ts.isStringLiteral)
56
- .map((element) => element.text)
57
- } else if (
58
- propName === 'errors' &&
59
- ts.isArrayLiteralExpression(prop.initializer)
60
- ) {
61
- docs.errors = prop.initializer.elements
62
- .filter(ts.isIdentifier)
63
- .map((element) => element.text as unknown as string)
64
- }
65
- }
66
- })
67
-
68
- return docs
69
- }
70
-
71
- // Handle string literals for other properties
72
- if (
73
- ts.isStringLiteral(initializer) ||
74
- ts.isNoSubstitutionTemplateLiteral(initializer)
75
- ) {
76
- return initializer.text
77
- } else {
78
- // Handle other initializer types if necessary
79
- return initializer.getText()
80
- }
81
- }
82
-
83
- return null
84
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { inspect } from './inspector.js'
2
- export type { TypesMap } from './types-map.js'
3
- export type * from './types.js'
4
- export type { InspectorState } from './types.js'
package/src/inspector.ts DELETED
@@ -1,63 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { visit } from './visit.js'
3
- import { TypesMap } from './types-map.js'
4
- import {
5
- InspectorState,
6
- InspectorHTTPState,
7
- InspectorFilters,
8
- } from './types.js'
9
-
10
- export const normalizeHTTPTypes = (
11
- httpState: InspectorHTTPState
12
- ): InspectorHTTPState => {
13
- return httpState
14
- }
15
-
16
- export const inspect = (
17
- routeFiles: string[],
18
- filters: InspectorFilters
19
- ): InspectorState => {
20
- const program = ts.createProgram(routeFiles, {
21
- target: ts.ScriptTarget.ESNext,
22
- module: ts.ModuleKind.CommonJS,
23
- })
24
- const checker = program.getTypeChecker()
25
- const sourceFiles = program.getSourceFiles()
26
-
27
- const state: InspectorState = {
28
- singletonServicesTypeImportMap: new Map(),
29
- sessionServicesTypeImportMap: new Map(),
30
- userSessionTypeImportMap: new Map(),
31
- singletonServicesFactories: new Map(),
32
- sessionServicesFactories: new Map(),
33
- configFactories: new Map(),
34
- http: {
35
- typesMap: new TypesMap(),
36
- metaInputTypes: new Map(),
37
- meta: [],
38
- files: new Set(),
39
- },
40
- channels: {
41
- typesMap: new TypesMap(),
42
- metaInputTypes: new Map(),
43
- files: new Set(),
44
- meta: [],
45
- },
46
- scheduledTasks: {
47
- meta: [],
48
- files: new Set(),
49
- },
50
- }
51
-
52
- for (const sourceFile of sourceFiles) {
53
- ts.forEachChild(sourceFile, (child) =>
54
- visit(checker, child, state, filters)
55
- )
56
- }
57
-
58
- // Normalise the typesMap
59
-
60
- state.http = normalizeHTTPTypes(state.http)
61
-
62
- return state
63
- }
package/src/types-map.ts DELETED
@@ -1,130 +0,0 @@
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 DELETED
@@ -1,62 +0,0 @@
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 type InspectorFilters = {
47
- tags?: string[]
48
- }
49
- export interface InspectorState {
50
- singletonServicesTypeImportMap: PathToNameAndType
51
- sessionServicesTypeImportMap: PathToNameAndType
52
- userSessionTypeImportMap: PathToNameAndType
53
- singletonServicesFactories: PathToNameAndType
54
- sessionServicesFactories: PathToNameAndType
55
- configFactories: PathToNameAndType
56
- http: InspectorHTTPState
57
- channels: InspectorChannelState
58
- scheduledTasks: {
59
- meta: ScheduledTasksMeta
60
- files: Set<string>
61
- }
62
- }
package/src/utils.ts DELETED
@@ -1,371 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { TypesMap } from './types-map.js'
3
- import { InspectorFilters } from './types.js'
4
-
5
- type FunctionTypes = {
6
- type: string | null
7
- inputTypes: ts.Type[]
8
- inputs: null | string[]
9
- outputTypes: ts.Type[]
10
- outputs: null | string[]
11
- }
12
-
13
- export const extractTypeKeys = (type: ts.Type): string[] => {
14
- return type.getProperties().map((symbol) => symbol.getName())
15
- }
16
-
17
- export const nullifyTypes = (type: string | null) => {
18
- if (
19
- type === 'void' ||
20
- type === 'undefined' ||
21
- type === 'unknown' ||
22
- type === 'any'
23
- ) {
24
- return null
25
- }
26
- return type
27
- }
28
-
29
- const isValidVariableName = (name: string) => {
30
- const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
31
- return regex.test(name)
32
- }
33
-
34
- export const getNamesAndTypes = (
35
- checker: ts.TypeChecker,
36
- typesMap: TypesMap,
37
- direction: 'Input' | 'Output',
38
- funcName: string,
39
- type: ts.Type
40
- ) => {
41
- const result: {
42
- names: Set<string>
43
- types: ts.Type[]
44
- } = {
45
- names: new Set(),
46
- types: [],
47
- }
48
-
49
- const { names, types } = resolveUnionTypes(checker, type)
50
- const firstName = names[0]
51
- if (names.length > 1 || (firstName && !isValidVariableName(firstName))) {
52
- const aliasType = names.join(' | ')
53
- const aliasName = `${funcName.charAt(0).toUpperCase()}${funcName.slice(1)}${direction}`
54
-
55
- result.names = new Set([aliasName])
56
- result.types = types
57
-
58
- const references = types
59
- .map((t) => resolveTypeImports(t, typesMap, true))
60
- .flat()
61
- typesMap.addCustomType(aliasName, aliasType, references)
62
- } else {
63
- const uniqueNames = names
64
- .map((name, i) => {
65
- const type = types[i]
66
- if (!type) {
67
- throw new Error('TODO: Expected a type here to match name')
68
- }
69
- if (isPrimitiveType(type)) {
70
- return name
71
- }
72
- return resolveTypeImports(type, typesMap, false)
73
- })
74
- .flat()
75
- result.names = new Set(uniqueNames)
76
- result.types = types
77
- }
78
-
79
- return {
80
- names: Array.from(result.names),
81
- types: result.types,
82
- }
83
- }
84
-
85
- export const isPrimitiveType = (type: ts.Type): boolean => {
86
- const primitiveFlags =
87
- ts.TypeFlags.Number |
88
- ts.TypeFlags.String |
89
- ts.TypeFlags.Boolean |
90
- ts.TypeFlags.BigInt |
91
- ts.TypeFlags.ESSymbol |
92
- ts.TypeFlags.Void |
93
- ts.TypeFlags.Undefined |
94
- ts.TypeFlags.Null |
95
- ts.TypeFlags.Any |
96
- ts.TypeFlags.Unknown
97
-
98
- return (type.flags & primitiveFlags) !== 0
99
- }
100
-
101
- export const resolveUnionTypes = (
102
- checker: ts.TypeChecker,
103
- type: ts.Type
104
- ): { types: ts.Type[]; names: string[] } => {
105
- const types: ts.Type[] = []
106
- const names: string[] = []
107
-
108
- // Check if it's a union type AND not part of an intersection
109
- if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
110
- for (const t of type.types) {
111
- const name = nullifyTypes(checker.typeToString(t))
112
- if (name) {
113
- types.push(t)
114
- names.push(name)
115
- }
116
- }
117
- } else {
118
- const name = nullifyTypes(checker.typeToString(type))
119
- if (name) {
120
- types.push(type)
121
- names.push(name)
122
- }
123
- }
124
-
125
- return { types, names }
126
- }
127
-
128
- export const resolveTypeImports = (
129
- type: ts.Type,
130
- resolvedTypes: TypesMap,
131
- isCustom: boolean
132
- ): string[] => {
133
- const types: string[] = []
134
-
135
- const visitType = (currentType: ts.Type) => {
136
- const symbol = currentType.aliasSymbol || currentType.getSymbol()
137
-
138
- if (symbol) {
139
- const declarations = symbol.getDeclarations()
140
- const declaration = declarations?.[0]
141
- if (declaration) {
142
- const sourceFile = declaration.getSourceFile()
143
- const path = sourceFile.fileName
144
-
145
- // Skip built-in utility types or TypeScript lib types
146
- if (
147
- !path.includes('node_modules/typescript') &&
148
- symbol.getName() !== '__type' &&
149
- !isPrimitiveType(currentType)
150
- ) {
151
- const originalName = symbol.getName()
152
- // Check if the type is already in the map
153
- let uniqueName = resolvedTypes.exists(originalName, path)
154
- if (!uniqueName) {
155
- if (isCustom) {
156
- uniqueName = resolvedTypes.addUniqueType(originalName, path)
157
- } else {
158
- resolvedTypes.addType(originalName, path)
159
- uniqueName = originalName
160
- }
161
- }
162
- types.push(uniqueName)
163
- }
164
- }
165
- }
166
-
167
- if (isCustom) {
168
- // Handle nested utility types like Partial, Pick, etc.
169
- if (currentType.aliasTypeArguments) {
170
- currentType.aliasTypeArguments.forEach(visitType)
171
- }
172
-
173
- // Handle intersections and unions
174
- if (currentType.isUnionOrIntersection()) {
175
- currentType.types.forEach(visitType)
176
- }
177
-
178
- // Handle object types with type arguments
179
- if (
180
- currentType.flags & ts.TypeFlags.Object &&
181
- (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
182
- ) {
183
- const typeRef = currentType as ts.TypeReference
184
- typeRef.typeArguments?.forEach(visitType)
185
- }
186
- }
187
- }
188
-
189
- visitType(type)
190
- return types
191
- }
192
-
193
- export const getPropertyAssignment = (
194
- obj: ts.ObjectLiteralExpression,
195
- name: string
196
- ) => {
197
- const property = obj.properties.find(
198
- (p) =>
199
- (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
200
- ts.isIdentifier(p.name) &&
201
- p.name.text === name
202
- )
203
- if (!property) {
204
- console.error(`Missing property '${name}' in object`)
205
- return null
206
- }
207
- return property
208
- }
209
-
210
- export const getTypeArgumentsOfType = (
211
- checker: ts.TypeChecker,
212
- type: ts.Type
213
- ): readonly ts.Type[] | null => {
214
- if (type.isUnionOrIntersection()) {
215
- const types: ts.Type[] = []
216
- for (const subType of type.types) {
217
- const subTypeArgs = getTypeArgumentsOfType(checker, subType)
218
- if (subTypeArgs) {
219
- types.push(...subTypeArgs)
220
- }
221
- }
222
- return types.length > 0 ? types : null
223
- }
224
-
225
- // If the type is a TypeReference with typeArguments, return them
226
- if (
227
- type.flags & ts.TypeFlags.Object &&
228
- (type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
229
- ) {
230
- const typeRef = type as ts.TypeReference
231
- if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
232
- return typeRef.typeArguments
233
- }
234
- }
235
-
236
- // If the type is an alias with aliasTypeArguments, return them
237
- if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
238
- return type.aliasTypeArguments as ts.Type[]
239
- }
240
-
241
- return null
242
- }
243
-
244
- export const getFunctionTypes = (
245
- checker: ts.TypeChecker,
246
- obj: ts.ObjectLiteralExpression,
247
- {
248
- typesMap,
249
- funcName,
250
- subFunctionName = funcName,
251
- inputIndex,
252
- outputIndex,
253
- }: {
254
- typesMap: TypesMap
255
- subFunctionName?: string
256
- funcName: string
257
- inputIndex: number
258
- outputIndex: number
259
- }
260
- ): FunctionTypes => {
261
- const result: FunctionTypes = {
262
- inputTypes: [],
263
- inputs: null,
264
- outputTypes: [],
265
- outputs: null,
266
- type: null,
267
- }
268
-
269
- const property = getPropertyAssignment(obj, subFunctionName)
270
- if (!property) {
271
- return result
272
- }
273
-
274
- let type: ts.Type | undefined
275
-
276
- // Handle shorthand property assignment
277
- if (ts.isShorthandPropertyAssignment(property)) {
278
- const symbol = checker.getShorthandAssignmentValueSymbol(property)
279
- if (symbol) {
280
- type = checker.getTypeOfSymbolAtLocation(symbol, property)
281
- if (funcName === 'func') {
282
- funcName = symbol.name
283
- }
284
- }
285
- }
286
- // Handle regular property assignment
287
- else if (ts.isPropertyAssignment(property)) {
288
- if (ts.isObjectLiteralExpression(property.initializer)) {
289
- return getFunctionTypes(checker, property.initializer, {
290
- typesMap,
291
- funcName,
292
- subFunctionName: 'func',
293
- inputIndex,
294
- outputIndex,
295
- })
296
- }
297
-
298
- if (property.initializer) {
299
- type = checker.getTypeAtLocation(property.initializer)
300
- if (funcName === 'func') {
301
- funcName = property.initializer.getText()
302
- }
303
- }
304
- }
305
-
306
- if (!type) {
307
- console.error(`Unable to resolve type for property '${funcName}'`)
308
- return result
309
- }
310
-
311
- result.type = type.aliasSymbol?.getEscapedName() || null
312
-
313
- // Access type arguments from TypeReference
314
- const typeArguments = getTypeArgumentsOfType(checker, type)
315
-
316
- if (!typeArguments || typeArguments.length === 0) {
317
- // This is the case for inline functions. In this case we would want to
318
- // get the types from the second argument of the function...
319
- console.error(
320
- `\x1b[31m• No generic type arguments found for ${funcName}. Support for inline functions is not yet implemented.\x1b[0m`
321
- )
322
- return result
323
- }
324
-
325
- if (inputIndex !== undefined && inputIndex < typeArguments.length) {
326
- const { names, types } = getNamesAndTypes(
327
- checker,
328
- typesMap,
329
- 'Input',
330
- funcName,
331
- typeArguments[inputIndex]!
332
- )
333
- result.inputs = names
334
- result.inputTypes = types
335
- } else {
336
- console.log(`No input defined for ${funcName}`)
337
- }
338
-
339
- if (outputIndex !== undefined && outputIndex < typeArguments.length) {
340
- const { names, types } = getNamesAndTypes(
341
- checker,
342
- typesMap,
343
- 'Output',
344
- funcName,
345
- typeArguments[outputIndex]!
346
- )
347
- result.outputs = names
348
- result.outputTypes = types
349
- } else {
350
- console.info(`No output defined for ${funcName}`)
351
- }
352
-
353
- return result
354
- }
355
-
356
- export const matchesFilters = (
357
- filters: InspectorFilters,
358
- params: { tags?: string[] },
359
- meta: { type: 'schedule' | 'http' | 'channel'; name: string }
360
- ) => {
361
- if (Object.keys(filters).length === 0 || filters.tags?.length === 0) {
362
- return true
363
- }
364
-
365
- if (filters.tags?.some((tag) => params.tags?.includes(tag))) {
366
- return true
367
- }
368
-
369
- console.debug(`⒡ Filtered: ${meta.type}:${meta.name}`)
370
- return false
371
- }