@pikku/inspector 0.6.3 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pikku/inspector
2
2
 
3
+ ## 0.7.0
4
+
5
+ This has changed significantly. The inspector now finds all functions and then links them to events.
6
+
7
+ This means we can now get:
8
+
9
+ - RPCs out of the box
10
+ - Schemas are per function, not event
11
+ - Supports inline functions, external functions, anonymous functions
12
+
13
+ ## 0.6.4
14
+
15
+ ### Patch Changes
16
+
17
+ - 60b2265: refactor: supporting request and response objects
18
+ - Updated dependencies [60b2265]
19
+ - @pikku/core@0.6.22
20
+
3
21
  ## 0.6.3
4
22
 
5
23
  ### Patch Changes
@@ -1,3 +1,13 @@
1
1
  import * as ts from 'typescript';
2
- import { InspectorFilters, InspectorState } from './types.js';
3
- export declare const addChannel: (node: ts.Node, checker: ts.TypeChecker, state: InspectorState, filters: InspectorFilters) => void;
2
+ import type { ChannelMeta } from '@pikku/core/channel';
3
+ import type { InspectorFilters, InspectorState } from './types.js';
4
+ /**
5
+ * Build out the nested message-routes by looking up each handler
6
+ * in state.functions.meta instead of re-inferring it here.
7
+ */
8
+ export declare function addMessagesRoutes(obj: ts.ObjectLiteralExpression, state: InspectorState, checker: ts.TypeChecker): ChannelMeta['messageRoutes'];
9
+ /**
10
+ * Inspect addChannel calls, look up all handlers in state.functions.meta,
11
+ * and emit one entry into state.channels.meta.
12
+ */
13
+ export declare function addChannel(node: ts.Node, checker: ts.TypeChecker, state: InspectorState, filters: InspectorFilters): void;
@@ -2,127 +2,355 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue } from './get-property-value.js';
3
3
  import { pathToRegexp } from 'path-to-regexp';
4
4
  import { getInputTypes } from './add-http-route.js';
5
- import { getPropertyAssignment, getFunctionTypes, matchesFilters, } from './utils.js';
6
- const addMessagesRoutes = (obj, checker, typesMap) => {
7
- const messageTypes = {};
8
- // Find the onMessageRoute property
9
- const messagesProperty = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
10
- ts.isIdentifier(p.name) &&
11
- p.name.text === 'onMessageRoute');
12
- if (!messagesProperty || !ts.isPropertyAssignment(messagesProperty)) {
13
- console.log('onMessageRoute property not found or is not a valid assignment.');
14
- return {};
5
+ import { extractFunctionName, getPropertyAssignmentInitializer, matchesFilters, } from './utils.js';
6
+ /**
7
+ * Safely get the “initializer” expression of a property-like AST node:
8
+ * - for `foo: expr`, returns `expr`
9
+ * - for `{ foo }` shorthand, returns the identifier `foo`
10
+ * - otherwise, returns undefined
11
+ */
12
+ function getInitializerOf(elem) {
13
+ if (ts.isPropertyAssignment(elem)) {
14
+ return elem.initializer;
15
15
  }
16
- const initializer = messagesProperty.initializer;
17
- // Ensure initializer is an object literal expression
18
- if (!ts.isObjectLiteralExpression(initializer)) {
19
- console.log('onMessageRoute is not an object literal.');
20
- return {};
16
+ if (ts.isShorthandPropertyAssignment(elem)) {
17
+ return elem.name;
21
18
  }
22
- // Iterate over the first level properties (like 'event')
23
- initializer.properties.forEach((property) => {
24
- const channel = property.name.getText();
25
- messageTypes[channel] = {};
26
- if (ts.isPropertyAssignment(property)) {
27
- const nestedObject = property.initializer;
28
- if (ts.isObjectLiteralExpression(nestedObject)) {
29
- const keys = nestedObject.properties.map((p) => p.name?.getText());
30
- for (const route of keys) {
31
- if (route) {
32
- const result = getFunctionTypes(checker, nestedObject, {
33
- funcName: route,
34
- inputIndex: 0,
35
- outputIndex: 1,
36
- typesMap,
19
+ return undefined;
20
+ }
21
+ /**
22
+ * Resolve a handler expression (Identifier, CallExpression, or { func })
23
+ * into its underlying function name.
24
+ */
25
+ function getHandlerNameFromExpression(expr, checker) {
26
+ // Handle direct identifier case (which includes shorthand properties)
27
+ if (ts.isIdentifier(expr)) {
28
+ const sym = checker.getSymbolAtLocation(expr);
29
+ if (sym) {
30
+ let resolvedSym = sym;
31
+ if (resolvedSym.flags & ts.SymbolFlags.Alias) {
32
+ resolvedSym = checker.getAliasedSymbol(resolvedSym) ?? resolvedSym;
33
+ }
34
+ // Try to get declarations
35
+ const decls = resolvedSym.declarations ?? [];
36
+ if (decls.length > 0) {
37
+ const decl = decls[0];
38
+ // For variable declarations, look at the initializer
39
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
40
+ if (ts.isCallExpression(decl.initializer) ||
41
+ ts.isArrowFunction(decl.initializer) ||
42
+ ts.isFunctionExpression(decl.initializer)) {
43
+ // Extract function name from the declaration's initializer
44
+ const { pikkuFuncName } = extractFunctionName(decl.initializer, checker);
45
+ return pikkuFuncName;
46
+ }
47
+ }
48
+ // For function declarations, use directly
49
+ else if (ts.isFunctionDeclaration(decl)) {
50
+ const { pikkuFuncName } = extractFunctionName(decl, checker);
51
+ return pikkuFuncName;
52
+ }
53
+ }
54
+ }
55
+ // Fallback: try to extract directly from the identifier
56
+ const { pikkuFuncName } = extractFunctionName(expr, checker);
57
+ return pikkuFuncName;
58
+ }
59
+ // Handle call expressions
60
+ if (ts.isCallExpression(expr)) {
61
+ const { pikkuFuncName } = extractFunctionName(expr, checker);
62
+ return pikkuFuncName;
63
+ }
64
+ // Handle object literals with 'func' property
65
+ if (ts.isObjectLiteralExpression(expr)) {
66
+ const fnProp = getPropertyAssignmentInitializer(expr, 'func', true, checker);
67
+ if (fnProp) {
68
+ return getHandlerNameFromExpression(fnProp, checker);
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ /**
74
+ * Build out the nested message-routes by looking up each handler
75
+ * in state.functions.meta instead of re-inferring it here.
76
+ */
77
+ export function addMessagesRoutes(obj, state, checker) {
78
+ const result = {};
79
+ const onMsgRouteProp = getPropertyAssignmentInitializer(obj, 'onMessageRoute', true, checker);
80
+ if (!onMsgRouteProp)
81
+ return result;
82
+ if (!onMsgRouteProp || !ts.isObjectLiteralExpression(onMsgRouteProp))
83
+ return result;
84
+ for (const chanElem of onMsgRouteProp.properties) {
85
+ const chanInit = getInitializerOf(chanElem);
86
+ if (!chanInit || !ts.isObjectLiteralExpression(chanInit))
87
+ continue;
88
+ const channelKey = chanElem.name.getText();
89
+ result[channelKey] = {};
90
+ for (const routeElem of chanInit.properties) {
91
+ const init = getInitializerOf(routeElem);
92
+ if (!init)
93
+ continue;
94
+ const routeKey = routeElem.name.getText();
95
+ // For shorthand properties, we need to resolve the identifier to its declaration
96
+ if (ts.isShorthandPropertyAssignment(routeElem)) {
97
+ // Get the symbol for the shorthand property
98
+ const shorthandSym = checker.getShorthandAssignmentValueSymbol(routeElem);
99
+ if (shorthandSym &&
100
+ shorthandSym.declarations &&
101
+ shorthandSym.declarations.length > 0) {
102
+ const shorthandDecl = shorthandSym.declarations[0];
103
+ if (!shorthandDecl) {
104
+ throw new Error(`No declaration found for shorthand property '${routeKey}'`);
105
+ }
106
+ // Handle import specifiers
107
+ if (ts.isImportSpecifier(shorthandDecl)) {
108
+ // Get the imported symbol
109
+ const importedSymbol = checker.getSymbolAtLocation(shorthandDecl.name);
110
+ if (importedSymbol) {
111
+ // Try to resolve the alias to get the original symbol
112
+ let resolvedSymbol = importedSymbol;
113
+ if (resolvedSymbol.flags & ts.SymbolFlags.Alias) {
114
+ resolvedSymbol =
115
+ checker.getAliasedSymbol(resolvedSymbol) ?? resolvedSymbol;
116
+ }
117
+ // Try to get the declarations of the resolved symbol
118
+ const importDecls = resolvedSymbol.declarations ?? [];
119
+ if (importDecls.length > 0) {
120
+ const importDecl = importDecls[0];
121
+ // Handle different kinds of declarations
122
+ if (ts.isVariableDeclaration(importDecl) &&
123
+ importDecl.initializer) {
124
+ // Extract from the initializer if it's a function
125
+ if (ts.isArrowFunction(importDecl.initializer) ||
126
+ ts.isFunctionExpression(importDecl.initializer) ||
127
+ ts.isCallExpression(importDecl.initializer)) {
128
+ const { pikkuFuncName } = extractFunctionName(importDecl.initializer, checker);
129
+ const handlerName = pikkuFuncName;
130
+ // Look up in the registry
131
+ const fnMeta = state.functions.meta[handlerName];
132
+ if (fnMeta) {
133
+ result[channelKey][routeKey] = {
134
+ pikkuFuncName: handlerName,
135
+ inputs: fnMeta.inputs ?? null,
136
+ outputs: fnMeta.outputs ?? null,
137
+ };
138
+ continue;
139
+ }
140
+ }
141
+ }
142
+ else if (ts.isFunctionDeclaration(importDecl)) {
143
+ // Extract from the function declaration
144
+ const { pikkuFuncName } = extractFunctionName(importDecl, checker);
145
+ const handlerName = pikkuFuncName;
146
+ // Look up in the registry
147
+ const fnMeta = state.functions.meta[handlerName];
148
+ if (fnMeta) {
149
+ result[channelKey][routeKey] = {
150
+ pikkuFuncName: handlerName,
151
+ inputs: fnMeta.inputs ?? null,
152
+ outputs: fnMeta.outputs ?? null,
153
+ };
154
+ continue;
155
+ }
156
+ }
157
+ else if (ts.isExportSpecifier(importDecl)) {
158
+ // For re-exports, we need to follow another level of indirection
159
+ const exportSymbol = checker.getSymbolAtLocation(importDecl.name);
160
+ if (exportSymbol) {
161
+ let resolvedExportSymbol = exportSymbol;
162
+ if (resolvedExportSymbol.flags & ts.SymbolFlags.Alias) {
163
+ resolvedExportSymbol =
164
+ checker.getAliasedSymbol(resolvedExportSymbol) ??
165
+ resolvedExportSymbol;
166
+ }
167
+ const exportDecls = resolvedExportSymbol.declarations ?? [];
168
+ if (exportDecls.length > 0) {
169
+ const exportDecl = exportDecls[0];
170
+ if (ts.isVariableDeclaration(exportDecl) &&
171
+ exportDecl.initializer) {
172
+ const { pikkuFuncName } = extractFunctionName(exportDecl.initializer, checker);
173
+ const handlerName = pikkuFuncName;
174
+ const fnMeta = state.functions.meta[handlerName];
175
+ if (fnMeta) {
176
+ result[channelKey][routeKey] = {
177
+ pikkuFuncName: handlerName,
178
+ inputs: fnMeta.inputs ?? null,
179
+ outputs: fnMeta.outputs ?? null,
180
+ };
181
+ continue;
182
+ }
183
+ }
184
+ else if (ts.isFunctionDeclaration(exportDecl)) {
185
+ const { pikkuFuncName } = extractFunctionName(exportDecl, checker);
186
+ const handlerName = pikkuFuncName;
187
+ const fnMeta = state.functions.meta[handlerName];
188
+ if (fnMeta) {
189
+ result[channelKey][routeKey] = {
190
+ pikkuFuncName: handlerName,
191
+ inputs: fnMeta.inputs ?? null,
192
+ outputs: fnMeta.outputs ?? null,
193
+ };
194
+ continue;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // As a fallback, try to look up by name
203
+ const funcName = shorthandDecl.name.getText();
204
+ // Look for any function in the registry that ends with this name
205
+ const possibleMatch = Object.keys(state.functions.meta).find((key) => {
206
+ const parts = key.split('_');
207
+ const filename = parts[parts.length - 3] || '';
208
+ return filename.endsWith(funcName);
37
209
  });
38
- const inputs = result?.inputs || null;
39
- const outputs = result?.outputs || null;
40
- messageTypes[channel][route] = { inputs, outputs };
210
+ if (possibleMatch) {
211
+ const fnMeta = state.functions.meta[possibleMatch];
212
+ if (!fnMeta) {
213
+ console.error(`No function metadata found for handler '${possibleMatch}'`);
214
+ continue;
215
+ }
216
+ result[channelKey][routeKey] = {
217
+ pikkuFuncName: possibleMatch,
218
+ inputs: fnMeta.inputs ?? null,
219
+ outputs: fnMeta.outputs ?? null,
220
+ };
221
+ continue;
222
+ }
223
+ }
224
+ else {
225
+ // Handle other declaration types (variable, function, etc.)
226
+ let actualFunction = undefined;
227
+ if (ts.isVariableDeclaration(shorthandDecl)) {
228
+ // Check if it has an initializer
229
+ if (shorthandDecl.initializer) {
230
+ // If it's a function expression or similar, use that
231
+ if (ts.isArrowFunction(shorthandDecl.initializer) ||
232
+ ts.isFunctionExpression(shorthandDecl.initializer) ||
233
+ ts.isCallExpression(shorthandDecl.initializer)) {
234
+ actualFunction = shorthandDecl.initializer;
235
+ }
236
+ }
237
+ }
238
+ else if (ts.isFunctionDeclaration(shorthandDecl)) {
239
+ actualFunction = shorthandDecl;
240
+ }
241
+ // If we found the actual function, extract its name
242
+ if (actualFunction) {
243
+ // Extract the function name directly from the actual function
244
+ const { pikkuFuncName } = extractFunctionName(actualFunction, checker);
245
+ const handlerName = pikkuFuncName;
246
+ // Now use this handlerName to look up in the registry
247
+ const fnMeta = state.functions.meta[handlerName];
248
+ if (fnMeta) {
249
+ result[channelKey][routeKey] = {
250
+ pikkuFuncName: handlerName,
251
+ inputs: fnMeta.inputs ?? null,
252
+ outputs: fnMeta.outputs ?? null,
253
+ };
254
+ continue; // Skip the normal processing below
255
+ }
256
+ }
41
257
  }
42
258
  }
43
259
  }
44
- else {
45
- console.warn('Nested property is not an object literal:', nestedObject);
260
+ // Normal processing for non-shorthand properties
261
+ const handlerName = getHandlerNameFromExpression(init, checker);
262
+ if (!handlerName) {
263
+ console.error(`Could not resolve handler for message route '${routeKey}'`);
264
+ continue;
46
265
  }
266
+ const fnMeta = state.functions.meta[handlerName];
267
+ if (!fnMeta) {
268
+ console.error(`No function metadata found for handler '${handlerName}'`);
269
+ continue;
270
+ }
271
+ result[channelKey][routeKey] = {
272
+ pikkuFuncName: handlerName,
273
+ inputs: fnMeta.inputs ?? null,
274
+ outputs: fnMeta.outputs ?? null,
275
+ };
47
276
  }
48
- else {
49
- console.warn(`Property "${property.getText()}" is a ${ts.SyntaxKind[property.kind]}`);
50
- }
51
- });
52
- return messageTypes;
53
- };
54
- export const addChannel = (node, checker, state, filters) => {
55
- if (!ts.isCallExpression(node)) {
56
- return;
57
277
  }
58
- const args = node.arguments;
59
- const firstArg = args[0];
60
- const expression = node.expression;
61
- // Check if the call is to addRoute
62
- if (!ts.isIdentifier(expression) || expression.text !== 'addChannel') {
278
+ return result;
279
+ }
280
+ /**
281
+ * Inspect addChannel calls, look up all handlers in state.functions.meta,
282
+ * and emit one entry into state.channels.meta.
283
+ */
284
+ export function addChannel(node, checker, state, filters) {
285
+ if (!ts.isCallExpression(node))
63
286
  return;
64
- }
65
- if (!firstArg) {
287
+ const { expression, arguments: args } = node;
288
+ if (!ts.isIdentifier(expression) || expression.text !== 'addChannel')
289
+ return;
290
+ const first = args[0];
291
+ if (!first || !ts.isObjectLiteralExpression(first))
292
+ return;
293
+ const obj = first;
294
+ const name = getPropertyValue(obj, 'name');
295
+ const route = getPropertyValue(obj, 'route') ?? '';
296
+ if (!name) {
297
+ console.error('Channel name is required');
66
298
  return;
67
299
  }
68
- let docs;
69
- let paramsValues = [];
70
- let queryValues = [];
71
- let tags = undefined;
72
- let inputType = null;
73
- let route = null;
74
- let name = null;
75
- // Check if the first argument is an object literal
76
- if (ts.isObjectLiteralExpression(firstArg)) {
77
- const obj = firstArg;
78
- name = getPropertyValue(obj, 'name');
79
- route = getPropertyValue(obj, 'route');
80
- if (!name) {
81
- console.error('Channel name is required');
82
- return;
83
- }
84
- if (route) {
85
- const { keys } = pathToRegexp(route);
86
- paramsValues = keys.reduce((result, { type, name }) => {
87
- if (type === 'param') {
88
- result.push(name);
89
- }
90
- return result;
91
- }, []);
300
+ // path parameters
301
+ const params = route
302
+ ? pathToRegexp(route)
303
+ .keys.filter((k) => k.type === 'param')
304
+ .map((k) => k.name)
305
+ : [];
306
+ const docs = getPropertyValue(obj, 'docs');
307
+ const tags = getPropertyValue(obj, 'tags');
308
+ const query = getPropertyValue(obj, 'query');
309
+ if (!matchesFilters(filters, { tags }, { type: 'channel', name }))
310
+ return;
311
+ const connect = getPropertyAssignmentInitializer(obj, 'onConnect', false, checker);
312
+ const disconnect = getPropertyAssignmentInitializer(obj, 'onDisconnect', false, checker);
313
+ // default onMessage handler
314
+ let message = null;
315
+ const onMsgProp = getPropertyAssignmentInitializer(obj, 'onMessage', false, checker);
316
+ if (onMsgProp) {
317
+ const handlerName = onMsgProp && getHandlerNameFromExpression(onMsgProp, checker);
318
+ const fnMeta = handlerName && state.functions.meta[handlerName];
319
+ if (!fnMeta) {
320
+ console.error(`No function metadata for onMessage handler '${handlerName}'`);
321
+ throw new Error();
92
322
  }
93
323
  else {
94
- route = '';
95
- }
96
- docs = getPropertyValue(obj, 'docs') || undefined;
97
- queryValues = getPropertyValue(obj, 'query') || [];
98
- tags = getPropertyValue(obj, 'tags') || undefined;
99
- const connect = !!getPropertyAssignment(obj, 'onConnect');
100
- const disconnect = !!getPropertyAssignment(obj, 'onDisconnect');
101
- const { inputs, outputs } = getFunctionTypes(checker, obj, {
102
- funcName: 'onMessage',
103
- inputIndex: 0,
104
- outputIndex: 1,
105
- typesMap: state.channels.typesMap,
106
- });
107
- const message = { inputs, outputs };
108
- const messageRoutes = addMessagesRoutes(obj, checker, state.channels.typesMap);
109
- if (!matchesFilters(filters, { tags }, { type: 'channel', name })) {
110
- return;
324
+ message = {
325
+ pikkuFuncName: extractFunctionName(onMsgProp, checker)
326
+ .pikkuFuncName,
327
+ inputs: fnMeta.inputs ?? null,
328
+ outputs: fnMeta.outputs ?? null,
329
+ };
111
330
  }
112
- state.channels.files.add(node.getSourceFile().fileName);
113
- state.channels.meta.push({
114
- name,
115
- route,
116
- input: inputType,
117
- params: paramsValues.length > 0 ? paramsValues : undefined,
118
- query: queryValues.length > 0 ? queryValues : undefined,
119
- inputTypes: getInputTypes(state.channels.metaInputTypes, 'get', inputType, queryValues, paramsValues),
120
- connect,
121
- disconnect,
122
- message,
123
- messageRoutes,
124
- docs,
125
- tags,
126
- });
127
331
  }
128
- };
332
+ // nested message-routes
333
+ const messageRoutes = addMessagesRoutes(obj, state, checker);
334
+ // record into state
335
+ state.channels.files.add(node.getSourceFile().fileName);
336
+ state.channels.meta[name] = {
337
+ name,
338
+ route,
339
+ input: null,
340
+ params: params.length ? params : undefined,
341
+ query: query?.length ? query : undefined,
342
+ inputTypes: getInputTypes(state.channels.metaInputTypes, 'get', message?.inputs?.[0] ?? null, query, params),
343
+ connectPikkuFuncName: connect
344
+ ? extractFunctionName(connect, checker).pikkuFuncName
345
+ : null,
346
+ connect: !!connect,
347
+ disconnectPikkuFuncName: disconnect
348
+ ? extractFunctionName(disconnect, checker).pikkuFuncName
349
+ : null,
350
+ disconnect: !!disconnect,
351
+ message,
352
+ messageRoutes,
353
+ docs: docs ?? undefined,
354
+ tags: tags ?? undefined,
355
+ };
356
+ }
@@ -0,0 +1,7 @@
1
+ import * as ts from 'typescript';
2
+ import { InspectorState, InspectorFilters } from './types.js';
3
+ /**
4
+ * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
5
+ * then push into state.functions.meta.
6
+ */
7
+ export declare function addFunctions(node: ts.Node, checker: ts.TypeChecker, state: InspectorState, filters: InspectorFilters): void;