@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.
@@ -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
+ }