@pikku/inspector 0.7.0 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/add-functions.js +10 -1
- package/package.json +1 -1
- package/src/add-channel.ts +482 -0
- package/src/add-file-extends-core-type.ts +50 -0
- package/src/add-file-with-config.ts +45 -0
- package/src/add-file-with-factory.ts +65 -0
- package/src/add-functions.ts +376 -0
- package/src/add-http-route.ts +123 -0
- package/src/add-schedule.ts +76 -0
- package/src/does-type-extend-core-type.ts +53 -0
- package/src/get-property-value.ts +84 -0
- package/src/index.ts +4 -0
- package/src/inspector.ts +75 -0
- package/src/types-map.ts +130 -0
- package/src/types.ts +58 -0
- package/src/utils.ts +863 -0
- package/src/visit.ts +67 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/events/add-channel.d.ts +0 -1
- package/dist/events/add-channel.js +0 -170
- package/dist/events/add-http-route.d.ts +0 -16
- package/dist/events/add-http-route.js +0 -83
- package/dist/events/add-schedule.d.ts +0 -3
- package/dist/events/add-schedule.js +0 -38
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { getPropertyValue } from './get-property-value.js'
|
|
3
|
+
import { pathToRegexp } from 'path-to-regexp'
|
|
4
|
+
import { HTTPMethod } from '@pikku/core/http'
|
|
5
|
+
import { APIDocs } from '@pikku/core'
|
|
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
|
+
*/
|
|
17
|
+
export const getInputTypes = (
|
|
18
|
+
metaTypes: Map<
|
|
19
|
+
string,
|
|
20
|
+
{ query?: string[]; params?: string[]; body?: string[] }
|
|
21
|
+
>,
|
|
22
|
+
methodType: string,
|
|
23
|
+
inputType: string | null,
|
|
24
|
+
queryValues: string[],
|
|
25
|
+
paramsValues: string[]
|
|
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
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Simplified addHTTPRoute: re-uses function metadata from state.functions.meta
|
|
40
|
+
* instead of re-inferring types here.
|
|
41
|
+
*/
|
|
42
|
+
export const addHTTPRoute = (
|
|
43
|
+
node: ts.Node,
|
|
44
|
+
checker: ts.TypeChecker,
|
|
45
|
+
state: InspectorState,
|
|
46
|
+
filters: InspectorFilters
|
|
47
|
+
) => {
|
|
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
|
|
53
|
+
|
|
54
|
+
// must pass an object literal
|
|
55
|
+
const firstArg = args[0]
|
|
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[]) || []
|
|
71
|
+
|
|
72
|
+
if (!matchesFilters(filters, { tags }, { type: 'http', name: route })) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
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}'.`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const funcName = extractFunctionName(funcInitializer, checker).pikkuFuncName
|
|
89
|
+
|
|
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
|
|
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
|
+
})
|
|
123
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { getPropertyValue } from './get-property-value.js'
|
|
3
|
+
import { APIDocs } from '@pikku/core'
|
|
4
|
+
import { InspectorFilters, InspectorState } from './types.js'
|
|
5
|
+
import { extractFunctionName, matchesFilters } from './utils.js'
|
|
6
|
+
|
|
7
|
+
export const addSchedule = (
|
|
8
|
+
node: ts.Node,
|
|
9
|
+
checker: ts.TypeChecker,
|
|
10
|
+
state: InspectorState,
|
|
11
|
+
filters: InspectorFilters
|
|
12
|
+
) => {
|
|
13
|
+
if (!ts.isCallExpression(node)) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const args = node.arguments
|
|
18
|
+
const firstArg = args[0]
|
|
19
|
+
const expression = node.expression
|
|
20
|
+
|
|
21
|
+
// Check if the call is to addScheduledTask
|
|
22
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'addScheduledTask') {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!firstArg) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
31
|
+
const obj = firstArg
|
|
32
|
+
|
|
33
|
+
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
34
|
+
const scheduleValue = getPropertyValue(obj, 'schedule') as string | null
|
|
35
|
+
const docs = (getPropertyValue(obj, 'docs') as APIDocs) || undefined
|
|
36
|
+
const tags = (getPropertyValue(obj, 'tags') as string[]) || undefined
|
|
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
|
+
|
|
57
|
+
if (!nameValue || !scheduleValue) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
!matchesFilters(filters, { tags }, { type: 'schedule', name: nameValue })
|
|
63
|
+
) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
state.scheduledTasks.files.add(node.getSourceFile().fileName)
|
|
68
|
+
state.scheduledTasks.meta[nameValue] = {
|
|
69
|
+
pikkuFuncName,
|
|
70
|
+
name: nameValue,
|
|
71
|
+
schedule: scheduleValue,
|
|
72
|
+
docs,
|
|
73
|
+
tags,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
export const doesTypeExtendsCore = (
|
|
4
|
+
type: ts.Type,
|
|
5
|
+
checker: ts.TypeChecker,
|
|
6
|
+
visitedTypes: Set<ts.Type>,
|
|
7
|
+
coreType: string
|
|
8
|
+
): boolean => {
|
|
9
|
+
if (!type || !checker) return false
|
|
10
|
+
|
|
11
|
+
// Avoid infinite recursion by checking if we've already visited this type
|
|
12
|
+
if (visitedTypes.has(type)) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
visitedTypes.add(type)
|
|
16
|
+
|
|
17
|
+
const typeSymbol = type.getSymbol()
|
|
18
|
+
if (typeSymbol) {
|
|
19
|
+
// Check if the type is the core type
|
|
20
|
+
if (typeSymbol.getName() === coreType) {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// For interface and class types, check their base types
|
|
25
|
+
if (type.isClassOrInterface()) {
|
|
26
|
+
const baseTypes = type.getBaseTypes() || []
|
|
27
|
+
for (const baseType of baseTypes) {
|
|
28
|
+
if (doesTypeExtendsCore(baseType, checker, visitedTypes, coreType)) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// For type aliases, get the aliased type
|
|
36
|
+
if (type.aliasSymbol) {
|
|
37
|
+
const aliasedType = checker.getDeclaredTypeOfSymbol(type.aliasSymbol)
|
|
38
|
+
if (doesTypeExtendsCore(aliasedType, checker, visitedTypes, coreType)) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// For union and intersection types, check all constituent types
|
|
44
|
+
if (type.isUnionOrIntersection()) {
|
|
45
|
+
for (const subType of type.types) {
|
|
46
|
+
if (doesTypeExtendsCore(subType, checker, visitedTypes, coreType)) {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|