@pikku/inspector 0.9.6-next.0 → 0.10.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 +6 -0
- package/dist/add/add-channel.d.ts +5 -1
- package/dist/add/add-channel.js +51 -32
- package/dist/add/add-cli.d.ts +4 -0
- package/dist/add/add-cli.js +128 -23
- package/dist/add/add-file-extends-core-type.js +3 -2
- package/dist/add/add-file-with-factory.d.ts +2 -2
- package/dist/add/add-file-with-factory.js +34 -1
- package/dist/add/add-functions.js +52 -5
- package/dist/add/add-http-route.js +19 -12
- package/dist/add/add-mcp-prompt.js +20 -13
- package/dist/add/add-mcp-resource.js +24 -14
- package/dist/add/add-mcp-tool.js +23 -13
- package/dist/add/add-middleware.js +51 -12
- package/dist/add/add-permission.d.ts +1 -2
- package/dist/add/add-permission.js +275 -19
- package/dist/add/add-queue-worker.js +10 -12
- package/dist/add/add-schedule.js +9 -10
- package/dist/error-codes.d.ts +35 -0
- package/dist/error-codes.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/inspector.js +20 -1
- package/dist/types.d.ts +31 -3
- package/dist/utils/ensure-function-metadata.d.ts +6 -0
- package/dist/utils/ensure-function-metadata.js +18 -0
- package/dist/utils/extract-function-name.d.ts +2 -2
- package/dist/utils/extract-function-name.js +13 -8
- package/dist/utils/filter-inspector-state.d.ts +6 -0
- package/dist/utils/filter-inspector-state.js +382 -0
- package/dist/utils/filter-utils.d.ts +10 -0
- package/dist/utils/filter-utils.js +66 -2
- package/dist/utils/find-root-dir.d.ts +23 -0
- package/dist/utils/find-root-dir.js +55 -0
- package/dist/utils/get-files-and-methods.d.ts +2 -1
- package/dist/utils/get-files-and-methods.js +2 -1
- package/dist/utils/get-property-value.d.ts +9 -0
- package/dist/utils/get-property-value.js +20 -0
- package/dist/utils/middleware.d.ts +1 -1
- package/dist/utils/middleware.js +7 -7
- package/dist/utils/permissions.d.ts +43 -0
- package/dist/utils/permissions.js +178 -0
- package/dist/utils/post-process.d.ts +16 -0
- package/dist/utils/post-process.js +132 -0
- package/dist/utils/serialize-inspector-state.d.ts +179 -0
- package/dist/utils/serialize-inspector-state.js +170 -0
- package/dist/visit.js +3 -2
- package/package.json +4 -4
- package/src/add/add-channel.ts +92 -40
- package/src/add/add-cli.ts +188 -29
- package/src/add/add-file-extends-core-type.ts +5 -2
- package/src/add/add-file-with-factory.ts +45 -2
- package/src/add/add-functions.ts +60 -5
- package/src/add/add-http-route.ts +46 -21
- package/src/add/add-mcp-prompt.ts +42 -21
- package/src/add/add-mcp-prompt.ts.tmp +0 -0
- package/src/add/add-mcp-resource.ts +50 -24
- package/src/add/add-mcp-resource.ts.tmp +0 -0
- package/src/add/add-mcp-tool.ts +48 -21
- package/src/add/add-middleware.ts +74 -15
- package/src/add/add-permission.ts +364 -22
- package/src/add/add-queue-worker.ts +22 -25
- package/src/add/add-schedule.ts +19 -20
- package/src/error-codes.ts +43 -0
- package/src/index.ts +7 -0
- package/src/inspector.ts +22 -1
- package/src/types.ts +38 -3
- package/src/utils/ensure-function-metadata.ts +24 -0
- package/src/utils/extract-function-name.ts +20 -8
- package/src/utils/filter-inspector-state.test.ts +1433 -0
- package/src/utils/filter-inspector-state.ts +526 -0
- package/src/utils/filter-utils.test.ts +350 -1
- package/src/utils/filter-utils.ts +82 -2
- package/src/utils/find-root-dir.ts +68 -0
- package/src/utils/get-files-and-methods.ts +8 -0
- package/src/utils/get-property-value.ts +27 -0
- package/src/utils/middleware.ts +14 -7
- package/src/utils/permissions.test.ts +327 -0
- package/src/utils/permissions.ts +262 -0
- package/src/utils/post-process.ts +178 -0
- package/src/utils/serialize-inspector-state.ts +375 -0
- package/src/utils/test-data/inspector-state.json +1680 -0
- package/src/visit.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @pikku/inspector
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
This release includes significant improvements across the framework including tree-shaking support, middleware/permission factories, enhanced CLI functionality, improved TypeScript type safety, and comprehensive test strategies.
|
|
6
|
+
|
|
7
|
+
For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
8
|
+
|
|
3
9
|
## 0.9.6-next.0
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
2
3
|
import type { ChannelMeta } from '@pikku/core/channel';
|
|
3
4
|
import type { InspectorState, AddWiring } from '../types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Build out the nested message-routes by looking up each handler
|
|
6
7
|
* in state.functions.meta instead of re-inferring it here.
|
|
7
8
|
*/
|
|
8
|
-
export declare function addMessagesRoutes(
|
|
9
|
+
export declare function addMessagesRoutes(logger: {
|
|
10
|
+
error: (msg: string) => void;
|
|
11
|
+
critical: (code: ErrorCode, msg: string) => void;
|
|
12
|
+
}, obj: ts.ObjectLiteralExpression, state: InspectorState, checker: ts.TypeChecker): ChannelMeta['messageWirings'];
|
|
9
13
|
/**
|
|
10
14
|
* Inspect addChannel calls, look up all handlers in state.functions.meta,
|
|
11
15
|
* and emit one entry into state.channels.meta.
|
package/dist/add/add-channel.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
import { getPropertyValue, getPropertyTags, } from '../utils/get-property-value.js';
|
|
3
4
|
import { pathToRegexp } from 'path-to-regexp';
|
|
4
|
-
import { PikkuWiringTypes } from '@pikku/core';
|
|
5
5
|
import { extractFunctionName } from '../utils/extract-function-name.js';
|
|
6
6
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
|
|
7
|
-
import { matchesFilters } from '../utils/filter-utils.js';
|
|
8
7
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
8
|
+
import { extractWireNames } from '../utils/post-process.js';
|
|
9
9
|
/**
|
|
10
10
|
* Safely get the "initializer" expression of a property-like AST node:
|
|
11
11
|
* - for `foo: expr`, returns `expr`
|
|
@@ -25,7 +25,7 @@ function getInitializerOf(elem) {
|
|
|
25
25
|
* Resolve a handler expression (Identifier, CallExpression, or { func })
|
|
26
26
|
* into its underlying function name.
|
|
27
27
|
*/
|
|
28
|
-
function getHandlerNameFromExpression(expr, checker) {
|
|
28
|
+
function getHandlerNameFromExpression(expr, checker, rootDir) {
|
|
29
29
|
// Handle direct identifier case (which includes shorthand properties)
|
|
30
30
|
if (ts.isIdentifier(expr)) {
|
|
31
31
|
const sym = checker.getSymbolAtLocation(expr);
|
|
@@ -44,31 +44,31 @@ function getHandlerNameFromExpression(expr, checker) {
|
|
|
44
44
|
ts.isArrowFunction(decl.initializer) ||
|
|
45
45
|
ts.isFunctionExpression(decl.initializer)) {
|
|
46
46
|
// Extract function name from the declaration's initializer
|
|
47
|
-
const { pikkuFuncName } = extractFunctionName(decl.initializer, checker);
|
|
47
|
+
const { pikkuFuncName } = extractFunctionName(decl.initializer, checker, rootDir);
|
|
48
48
|
return pikkuFuncName;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// For function declarations, use directly
|
|
52
52
|
else if (ts.isFunctionDeclaration(decl)) {
|
|
53
|
-
const { pikkuFuncName } = extractFunctionName(decl, checker);
|
|
53
|
+
const { pikkuFuncName } = extractFunctionName(decl, checker, rootDir);
|
|
54
54
|
return pikkuFuncName;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
// Fallback: try to extract directly from the identifier
|
|
59
|
-
const { pikkuFuncName } = extractFunctionName(expr, checker);
|
|
59
|
+
const { pikkuFuncName } = extractFunctionName(expr, checker, rootDir);
|
|
60
60
|
return pikkuFuncName;
|
|
61
61
|
}
|
|
62
62
|
// Handle call expressions
|
|
63
63
|
if (ts.isCallExpression(expr)) {
|
|
64
|
-
const { pikkuFuncName } = extractFunctionName(expr, checker);
|
|
64
|
+
const { pikkuFuncName } = extractFunctionName(expr, checker, rootDir);
|
|
65
65
|
return pikkuFuncName;
|
|
66
66
|
}
|
|
67
67
|
// Handle object literals with 'func' property
|
|
68
68
|
if (ts.isObjectLiteralExpression(expr)) {
|
|
69
69
|
const fnProp = getPropertyAssignmentInitializer(expr, 'func', true, checker);
|
|
70
70
|
if (fnProp) {
|
|
71
|
-
return getHandlerNameFromExpression(fnProp, checker);
|
|
71
|
+
return getHandlerNameFromExpression(fnProp, checker, rootDir);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
return null;
|
|
@@ -77,7 +77,7 @@ function getHandlerNameFromExpression(expr, checker) {
|
|
|
77
77
|
* Build out the nested message-routes by looking up each handler
|
|
78
78
|
* in state.functions.meta instead of re-inferring it here.
|
|
79
79
|
*/
|
|
80
|
-
export function addMessagesRoutes(obj, state, checker) {
|
|
80
|
+
export function addMessagesRoutes(logger, obj, state, checker) {
|
|
81
81
|
const result = {};
|
|
82
82
|
const onMsgRouteProp = getPropertyAssignmentInitializer(obj, 'onMessageWiring', true, checker);
|
|
83
83
|
if (!onMsgRouteProp)
|
|
@@ -128,7 +128,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
128
128
|
if (ts.isArrowFunction(importDecl.initializer) ||
|
|
129
129
|
ts.isFunctionExpression(importDecl.initializer) ||
|
|
130
130
|
ts.isCallExpression(importDecl.initializer)) {
|
|
131
|
-
const { pikkuFuncName } = extractFunctionName(importDecl.initializer, checker);
|
|
131
|
+
const { pikkuFuncName } = extractFunctionName(importDecl.initializer, checker, state.rootDir);
|
|
132
132
|
const handlerName = pikkuFuncName;
|
|
133
133
|
// Look up in the registry
|
|
134
134
|
const fnMeta = state.functions.meta[handlerName];
|
|
@@ -142,7 +142,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
142
142
|
}
|
|
143
143
|
else if (ts.isFunctionDeclaration(importDecl)) {
|
|
144
144
|
// Extract from the function declaration
|
|
145
|
-
const { pikkuFuncName } = extractFunctionName(importDecl, checker);
|
|
145
|
+
const { pikkuFuncName } = extractFunctionName(importDecl, checker, state.rootDir);
|
|
146
146
|
const handlerName = pikkuFuncName;
|
|
147
147
|
// Look up in the registry
|
|
148
148
|
const fnMeta = state.functions.meta[handlerName];
|
|
@@ -168,7 +168,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
168
168
|
const exportDecl = exportDecls[0];
|
|
169
169
|
if (ts.isVariableDeclaration(exportDecl) &&
|
|
170
170
|
exportDecl.initializer) {
|
|
171
|
-
const { pikkuFuncName } = extractFunctionName(exportDecl.initializer, checker);
|
|
171
|
+
const { pikkuFuncName } = extractFunctionName(exportDecl.initializer, checker, state.rootDir);
|
|
172
172
|
const handlerName = pikkuFuncName;
|
|
173
173
|
const fnMeta = state.functions.meta[handlerName];
|
|
174
174
|
if (fnMeta) {
|
|
@@ -179,7 +179,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
else if (ts.isFunctionDeclaration(exportDecl)) {
|
|
182
|
-
const { pikkuFuncName } = extractFunctionName(exportDecl, checker);
|
|
182
|
+
const { pikkuFuncName } = extractFunctionName(exportDecl, checker, state.rootDir);
|
|
183
183
|
const handlerName = pikkuFuncName;
|
|
184
184
|
const fnMeta = state.functions.meta[handlerName];
|
|
185
185
|
if (fnMeta) {
|
|
@@ -205,7 +205,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
205
205
|
if (possibleMatch) {
|
|
206
206
|
const fnMeta = state.functions.meta[possibleMatch];
|
|
207
207
|
if (!fnMeta) {
|
|
208
|
-
|
|
208
|
+
logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for handler '${possibleMatch}'`);
|
|
209
209
|
continue;
|
|
210
210
|
}
|
|
211
211
|
result[channelKey][routeKey] = {
|
|
@@ -234,7 +234,7 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
234
234
|
// If we found the actual function, extract its name
|
|
235
235
|
if (actualFunction) {
|
|
236
236
|
// Extract the function name directly from the actual function
|
|
237
|
-
const { pikkuFuncName } = extractFunctionName(actualFunction, checker);
|
|
237
|
+
const { pikkuFuncName } = extractFunctionName(actualFunction, checker, state.rootDir);
|
|
238
238
|
const handlerName = pikkuFuncName;
|
|
239
239
|
// Now use this handlerName to look up in the registry
|
|
240
240
|
const fnMeta = state.functions.meta[handlerName];
|
|
@@ -249,14 +249,14 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
// Normal processing for non-shorthand properties
|
|
252
|
-
const handlerName = getHandlerNameFromExpression(init, checker);
|
|
252
|
+
const handlerName = getHandlerNameFromExpression(init, checker, state.rootDir);
|
|
253
253
|
if (!handlerName) {
|
|
254
|
-
|
|
254
|
+
logger.error(`Could not resolve handler for message route '${routeKey}'`);
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
257
|
const fnMeta = state.functions.meta[handlerName];
|
|
258
258
|
if (!fnMeta) {
|
|
259
|
-
|
|
259
|
+
logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for handler '${handlerName}'`);
|
|
260
260
|
continue;
|
|
261
261
|
}
|
|
262
262
|
result[channelKey][routeKey] = {
|
|
@@ -271,7 +271,6 @@ export function addMessagesRoutes(obj, state, checker) {
|
|
|
271
271
|
* and emit one entry into state.channels.meta.
|
|
272
272
|
*/
|
|
273
273
|
export const addChannel = (logger, node, checker, state, options) => {
|
|
274
|
-
const filters = options.filters || {};
|
|
275
274
|
if (!ts.isCallExpression(node))
|
|
276
275
|
return;
|
|
277
276
|
const { expression, arguments: args } = node;
|
|
@@ -284,7 +283,7 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
284
283
|
const name = getPropertyValue(obj, 'name');
|
|
285
284
|
const route = getPropertyValue(obj, 'route') ?? '';
|
|
286
285
|
if (!name) {
|
|
287
|
-
|
|
286
|
+
logger.critical(ErrorCode.MISSING_CHANNEL_NAME, 'Channel name is required');
|
|
288
287
|
return;
|
|
289
288
|
}
|
|
290
289
|
// path parameters
|
|
@@ -294,18 +293,16 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
294
293
|
.map((k) => k.name)
|
|
295
294
|
: [];
|
|
296
295
|
const docs = getPropertyValue(obj, 'docs');
|
|
297
|
-
const tags =
|
|
296
|
+
const tags = getPropertyTags(obj, 'Channel', route, logger);
|
|
298
297
|
const query = getPropertyValue(obj, 'query');
|
|
299
|
-
const filePath = node.getSourceFile().fileName;
|
|
300
|
-
if (!matchesFilters(filters, { tags }, { type: PikkuWiringTypes.channel, name, filePath }, logger))
|
|
301
|
-
return;
|
|
302
298
|
const connect = getPropertyAssignmentInitializer(obj, 'onConnect', false, checker);
|
|
303
299
|
const disconnect = getPropertyAssignmentInitializer(obj, 'onDisconnect', false, checker);
|
|
304
300
|
// default onMessage handler
|
|
305
301
|
let message = null;
|
|
306
302
|
const onMsgProp = getPropertyAssignmentInitializer(obj, 'onMessage', false, checker);
|
|
307
303
|
if (onMsgProp) {
|
|
308
|
-
const handlerName = onMsgProp &&
|
|
304
|
+
const handlerName = onMsgProp &&
|
|
305
|
+
getHandlerNameFromExpression(onMsgProp, checker, state.rootDir);
|
|
309
306
|
const fnMeta = handlerName && state.functions.meta[handlerName];
|
|
310
307
|
if (!fnMeta) {
|
|
311
308
|
console.error(`No function metadata for onMessage handler '${handlerName}'`);
|
|
@@ -313,15 +310,35 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
313
310
|
}
|
|
314
311
|
else {
|
|
315
312
|
message = {
|
|
316
|
-
pikkuFuncName: extractFunctionName(onMsgProp, checker)
|
|
317
|
-
.pikkuFuncName,
|
|
313
|
+
pikkuFuncName: extractFunctionName(onMsgProp, checker, state.rootDir).pikkuFuncName,
|
|
318
314
|
};
|
|
319
315
|
}
|
|
320
316
|
}
|
|
321
317
|
// nested message-routes
|
|
322
|
-
const messageWirings = addMessagesRoutes(obj, state, checker);
|
|
318
|
+
const messageWirings = addMessagesRoutes(logger, obj, state, checker);
|
|
323
319
|
// --- resolve middleware ---
|
|
324
320
|
const middleware = resolveMiddleware(state, obj, tags, checker);
|
|
321
|
+
// --- track used functions/middleware for service aggregation ---
|
|
322
|
+
// Track connect/disconnect/message handlers
|
|
323
|
+
if (connect) {
|
|
324
|
+
const connectFuncName = extractFunctionName(connect, checker, state.rootDir).pikkuFuncName;
|
|
325
|
+
state.serviceAggregation.usedFunctions.add(connectFuncName);
|
|
326
|
+
}
|
|
327
|
+
if (disconnect) {
|
|
328
|
+
const disconnectFuncName = extractFunctionName(disconnect, checker, state.rootDir).pikkuFuncName;
|
|
329
|
+
state.serviceAggregation.usedFunctions.add(disconnectFuncName);
|
|
330
|
+
}
|
|
331
|
+
if (message) {
|
|
332
|
+
state.serviceAggregation.usedFunctions.add(message.pikkuFuncName);
|
|
333
|
+
}
|
|
334
|
+
// Track message wiring handlers
|
|
335
|
+
for (const channelHandlers of Object.values(messageWirings)) {
|
|
336
|
+
for (const handler of Object.values(channelHandlers)) {
|
|
337
|
+
state.serviceAggregation.usedFunctions.add(handler.pikkuFuncName);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Track middleware
|
|
341
|
+
extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
|
|
325
342
|
// record into state
|
|
326
343
|
state.channels.files.add(node.getSourceFile().fileName);
|
|
327
344
|
state.channels.meta[name] = {
|
|
@@ -338,12 +355,14 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
338
355
|
// params
|
|
339
356
|
// ),
|
|
340
357
|
connect: connect
|
|
341
|
-
? {
|
|
358
|
+
? {
|
|
359
|
+
pikkuFuncName: extractFunctionName(connect, checker, state.rootDir)
|
|
360
|
+
.pikkuFuncName,
|
|
361
|
+
}
|
|
342
362
|
: null,
|
|
343
363
|
disconnect: disconnect
|
|
344
364
|
? {
|
|
345
|
-
pikkuFuncName: extractFunctionName(disconnect, checker)
|
|
346
|
-
.pikkuFuncName,
|
|
365
|
+
pikkuFuncName: extractFunctionName(disconnect, checker, state.rootDir).pikkuFuncName,
|
|
347
366
|
}
|
|
348
367
|
: null,
|
|
349
368
|
message,
|
package/dist/add/add-cli.d.ts
CHANGED
package/dist/add/add-cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { extractFunctionName } from '../utils/extract-function-name.js';
|
|
3
3
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
4
|
+
import { extractWireNames } from '../utils/post-process.js';
|
|
4
5
|
import { getPropertyValue } from '../utils/get-property-value.js';
|
|
6
|
+
// Track if we've warned about missing Config type to avoid duplicate warnings
|
|
7
|
+
const configTypeWarningShown = new Set();
|
|
5
8
|
/**
|
|
6
9
|
* Adds CLI command metadata to the inspector state
|
|
7
10
|
*/
|
|
@@ -33,18 +36,39 @@ export const addCLI = (logger, node, typeChecker, inspectorState, options) => {
|
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
// Add this program to the CLI metadata
|
|
36
|
-
inspectorState.cli.meta[cliConfig.programName] =
|
|
39
|
+
inspectorState.cli.meta.programs[cliConfig.programName] =
|
|
40
|
+
cliConfig.programMeta;
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Processes a CLI configuration object
|
|
40
44
|
*/
|
|
41
45
|
function processCLIConfig(logger, node, sourceFile, typeChecker, inspectorState, options) {
|
|
42
46
|
let programName = '';
|
|
47
|
+
let programTags;
|
|
43
48
|
const programMeta = {
|
|
44
49
|
program: '',
|
|
45
50
|
commands: {},
|
|
46
51
|
options: {},
|
|
47
52
|
};
|
|
53
|
+
// First pass: extract program name and tags
|
|
54
|
+
for (const prop of node.properties) {
|
|
55
|
+
if (!ts.isPropertyAssignment(prop))
|
|
56
|
+
continue;
|
|
57
|
+
if (!ts.isIdentifier(prop.name))
|
|
58
|
+
continue;
|
|
59
|
+
const propName = prop.name.text;
|
|
60
|
+
if (propName === 'program' && ts.isStringLiteral(prop.initializer)) {
|
|
61
|
+
programName = prop.initializer.text;
|
|
62
|
+
programMeta.program = programName;
|
|
63
|
+
}
|
|
64
|
+
else if (propName === 'tags') {
|
|
65
|
+
programTags = getPropertyValue(node, 'tags') || undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!programName) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Second pass: process other properties with program tags available
|
|
48
72
|
for (const prop of node.properties) {
|
|
49
73
|
if (!ts.isPropertyAssignment(prop))
|
|
50
74
|
continue;
|
|
@@ -53,14 +77,12 @@ function processCLIConfig(logger, node, sourceFile, typeChecker, inspectorState,
|
|
|
53
77
|
const propName = prop.name.text;
|
|
54
78
|
switch (propName) {
|
|
55
79
|
case 'program':
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
programMeta.program = programName;
|
|
59
|
-
}
|
|
80
|
+
case 'tags':
|
|
81
|
+
// Already handled in first pass
|
|
60
82
|
break;
|
|
61
83
|
case 'commands':
|
|
62
84
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
63
|
-
programMeta.commands = processCommands(logger, prop.initializer, sourceFile, typeChecker, programName, inspectorState, options);
|
|
85
|
+
programMeta.commands = processCommands(logger, prop.initializer, sourceFile, typeChecker, programName, inspectorState, options, programTags);
|
|
64
86
|
}
|
|
65
87
|
break;
|
|
66
88
|
case 'options':
|
|
@@ -69,30 +91,39 @@ function processCLIConfig(logger, node, sourceFile, typeChecker, inspectorState,
|
|
|
69
91
|
}
|
|
70
92
|
break;
|
|
71
93
|
case 'render':
|
|
72
|
-
//
|
|
73
|
-
programMeta.defaultRenderName =
|
|
94
|
+
// Extract the actual renderer function name
|
|
95
|
+
programMeta.defaultRenderName = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncName;
|
|
74
96
|
break;
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
|
-
if (!programName) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
99
|
return { programName, programMeta };
|
|
81
100
|
}
|
|
82
101
|
/**
|
|
83
102
|
* Processes the commands object
|
|
84
103
|
*/
|
|
85
|
-
function processCommands(logger, node, sourceFile, typeChecker, programName, inspectorState, options) {
|
|
104
|
+
function processCommands(logger, node, sourceFile, typeChecker, programName, inspectorState, options, programTags) {
|
|
86
105
|
const commands = {};
|
|
106
|
+
let defaultCommandName = null;
|
|
87
107
|
for (const prop of node.properties) {
|
|
88
108
|
if (!ts.isPropertyAssignment(prop))
|
|
89
109
|
continue;
|
|
90
110
|
const commandName = getPropertyName(prop);
|
|
91
111
|
if (!commandName)
|
|
92
112
|
continue;
|
|
93
|
-
const commandMeta = processCommand(logger, inspectorState, options, commandName, prop.initializer, sourceFile, typeChecker, programName);
|
|
113
|
+
const commandMeta = processCommand(logger, inspectorState, options, commandName, prop.initializer, sourceFile, typeChecker, programName, [], programTags);
|
|
94
114
|
if (commandMeta) {
|
|
95
115
|
commands[commandName] = commandMeta;
|
|
116
|
+
// Validate only one default command
|
|
117
|
+
if (commandMeta.isDefault) {
|
|
118
|
+
if (defaultCommandName !== null) {
|
|
119
|
+
const position = prop.getStart(sourceFile);
|
|
120
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
|
|
121
|
+
throw new Error(`Multiple default commands found in CLI program "${programName}" at ${sourceFile.fileName}:${line + 1}:${character + 1}.\n` +
|
|
122
|
+
`Commands "${defaultCommandName}" and "${commandName}" are both marked as default.\n` +
|
|
123
|
+
`Only one command can be marked as default per program.`);
|
|
124
|
+
}
|
|
125
|
+
defaultCommandName = commandName;
|
|
126
|
+
}
|
|
96
127
|
}
|
|
97
128
|
}
|
|
98
129
|
return commands;
|
|
@@ -100,14 +131,14 @@ function processCommands(logger, node, sourceFile, typeChecker, programName, ins
|
|
|
100
131
|
/**
|
|
101
132
|
* Processes a single command
|
|
102
133
|
*/
|
|
103
|
-
function processCommand(logger, inspectorState, options, name, node, sourceFile, typeChecker, programName, parentPath = []) {
|
|
134
|
+
function processCommand(logger, inspectorState, options, name, node, sourceFile, typeChecker, programName, parentPath = [], programTags) {
|
|
104
135
|
const fullPath = [...parentPath, name];
|
|
105
136
|
// Handle shorthand (just a function)
|
|
106
137
|
if (ts.isIdentifier(node) ||
|
|
107
138
|
ts.isArrowFunction(node) ||
|
|
108
139
|
ts.isFunctionExpression(node)) {
|
|
109
140
|
return {
|
|
110
|
-
pikkuFuncName: extractFunctionName(node, typeChecker).pikkuFuncName,
|
|
141
|
+
pikkuFuncName: extractFunctionName(node, typeChecker, inspectorState.rootDir).pikkuFuncName,
|
|
111
142
|
positionals: [],
|
|
112
143
|
options: {},
|
|
113
144
|
};
|
|
@@ -120,7 +151,7 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
120
151
|
node.arguments.length > 0 &&
|
|
121
152
|
ts.isObjectLiteralExpression(node.arguments[0])) {
|
|
122
153
|
// Process the object literal argument
|
|
123
|
-
return processCommand(logger, inspectorState, options, name, node.arguments[0], sourceFile, typeChecker, programName, parentPath);
|
|
154
|
+
return processCommand(logger, inspectorState, options, name, node.arguments[0], sourceFile, typeChecker, programName, parentPath, programTags);
|
|
124
155
|
}
|
|
125
156
|
return null;
|
|
126
157
|
}
|
|
@@ -144,7 +175,7 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
144
175
|
continue;
|
|
145
176
|
const propName = prop.name.text;
|
|
146
177
|
if (propName === 'func') {
|
|
147
|
-
pikkuFuncName = extractFunctionName(prop.initializer, typeChecker).pikkuFuncName;
|
|
178
|
+
pikkuFuncName = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncName;
|
|
148
179
|
meta.pikkuFuncName = pikkuFuncName;
|
|
149
180
|
}
|
|
150
181
|
else if (propName === 'options' &&
|
|
@@ -155,11 +186,17 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
155
186
|
tags = getPropertyValue(node, 'tags') || undefined;
|
|
156
187
|
}
|
|
157
188
|
}
|
|
189
|
+
// Merge program-level tags with command-level tags
|
|
190
|
+
const allTags = [...(programTags || []), ...(tags || [])];
|
|
158
191
|
// Resolve middleware
|
|
159
|
-
const middleware = resolveMiddleware(inspectorState, node,
|
|
192
|
+
const middleware = resolveMiddleware(inspectorState, node, allTags.length > 0 ? allTags : undefined, typeChecker);
|
|
160
193
|
if (middleware) {
|
|
161
194
|
meta.middleware = middleware;
|
|
162
195
|
}
|
|
196
|
+
// Add merged tags to metadata
|
|
197
|
+
if (allTags.length > 0) {
|
|
198
|
+
meta.tags = allTags;
|
|
199
|
+
}
|
|
163
200
|
// Second pass: process all properties
|
|
164
201
|
for (const prop of node.properties) {
|
|
165
202
|
if (!ts.isPropertyAssignment(prop))
|
|
@@ -183,7 +220,7 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
183
220
|
// Already handled in first pass
|
|
184
221
|
break;
|
|
185
222
|
case 'render':
|
|
186
|
-
meta.renderName = extractFunctionName(prop.initializer, typeChecker).pikkuFuncName;
|
|
223
|
+
meta.renderName = extractFunctionName(prop.initializer, typeChecker, inspectorState.rootDir).pikkuFuncName;
|
|
187
224
|
break;
|
|
188
225
|
case 'options':
|
|
189
226
|
// Process with pikkuFuncName from first pass
|
|
@@ -200,15 +237,25 @@ function processCommand(logger, inspectorState, options, name, node, sourceFile,
|
|
|
200
237
|
const subName = getPropertyName(subProp);
|
|
201
238
|
if (!subName)
|
|
202
239
|
continue;
|
|
203
|
-
const subCommand = processCommand(logger, inspectorState, options, subName, subProp.initializer, sourceFile, typeChecker, programName, fullPath);
|
|
240
|
+
const subCommand = processCommand(logger, inspectorState, options, subName, subProp.initializer, sourceFile, typeChecker, programName, fullPath, programTags);
|
|
204
241
|
if (subCommand) {
|
|
205
242
|
meta.subcommands[subName] = subCommand;
|
|
206
243
|
}
|
|
207
244
|
}
|
|
208
245
|
}
|
|
209
246
|
break;
|
|
247
|
+
case 'isDefault':
|
|
248
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
249
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
250
|
+
meta.isDefault = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
210
253
|
}
|
|
211
254
|
}
|
|
255
|
+
// --- track used functions/middleware for service aggregation ---
|
|
256
|
+
inspectorState.serviceAggregation.usedFunctions.add(meta.pikkuFuncName);
|
|
257
|
+
extractWireNames(meta.middleware).forEach((name) => inspectorState.serviceAggregation.usedMiddleware.add(name));
|
|
258
|
+
// Note: subcommands are tracked recursively when they're processed
|
|
212
259
|
return meta;
|
|
213
260
|
}
|
|
214
261
|
/**
|
|
@@ -382,14 +429,21 @@ function extractEnumFromConfigType(logger, propertyName, typeChecker, inspectorS
|
|
|
382
429
|
// Look for Config type in typesLookup
|
|
383
430
|
const configTypes = inspectorState.typesLookup.get('Config');
|
|
384
431
|
if (!configTypes || configTypes.length === 0) {
|
|
385
|
-
|
|
386
|
-
|
|
432
|
+
// Only warn once per CLI file to avoid spamming logs
|
|
433
|
+
if (!configTypeWarningShown.has('missing-config-type')) {
|
|
434
|
+
configTypeWarningShown.add('missing-config-type');
|
|
435
|
+
logger.warn(`Could not find Config type in typesLookup. ` +
|
|
436
|
+
`Make sure you have a Config interface extending CoreConfig in your codebase.`);
|
|
437
|
+
}
|
|
387
438
|
return null;
|
|
388
439
|
}
|
|
389
440
|
// Use the first Config type (there should only be one)
|
|
390
441
|
const configType = configTypes[0];
|
|
391
442
|
if (!configType) {
|
|
392
|
-
|
|
443
|
+
if (!configTypeWarningShown.has('undefined-config-type')) {
|
|
444
|
+
configTypeWarningShown.add('undefined-config-type');
|
|
445
|
+
logger.warn(`Config type is undefined in typesLookup.`);
|
|
446
|
+
}
|
|
393
447
|
return null;
|
|
394
448
|
}
|
|
395
449
|
// Extract enum from the property
|
|
@@ -459,3 +513,54 @@ function parseCommandPattern(pattern) {
|
|
|
459
513
|
}
|
|
460
514
|
return positionals;
|
|
461
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Adds CLI renderer metadata to the inspector state
|
|
518
|
+
*/
|
|
519
|
+
export const addCLIRenderers = (logger, node, typeChecker, inspectorState, options) => {
|
|
520
|
+
if (!ts.isCallExpression(node))
|
|
521
|
+
return;
|
|
522
|
+
const { expression, arguments: args, typeArguments } = node;
|
|
523
|
+
// Only handle pikkuCLIRender calls
|
|
524
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (args.length === 0)
|
|
528
|
+
return;
|
|
529
|
+
// Extract renderer name
|
|
530
|
+
const { pikkuFuncName, exportedName } = extractFunctionName(node, typeChecker, inspectorState.rootDir);
|
|
531
|
+
// Get the source file path
|
|
532
|
+
const sourceFile = node.getSourceFile();
|
|
533
|
+
const filePath = sourceFile.fileName;
|
|
534
|
+
// Extract services from type parameters (second type param is Services)
|
|
535
|
+
const services = {
|
|
536
|
+
optimized: true,
|
|
537
|
+
services: [],
|
|
538
|
+
};
|
|
539
|
+
if (typeArguments && typeArguments.length >= 2) {
|
|
540
|
+
// Second type parameter is the Services type
|
|
541
|
+
const servicesTypeNode = typeArguments[1];
|
|
542
|
+
if (servicesTypeNode) {
|
|
543
|
+
const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode);
|
|
544
|
+
// Extract property names from the Services type
|
|
545
|
+
const properties = servicesType.getProperties();
|
|
546
|
+
for (const prop of properties) {
|
|
547
|
+
services.services.push(prop.getName());
|
|
548
|
+
}
|
|
549
|
+
// If no specific services found, it might be using the full services object
|
|
550
|
+
if (properties.length === 0) {
|
|
551
|
+
services.optimized = false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Store renderer metadata
|
|
556
|
+
inspectorState.cli.meta.renderers[pikkuFuncName] = {
|
|
557
|
+
name: pikkuFuncName,
|
|
558
|
+
exportedName: exportedName ?? undefined,
|
|
559
|
+
services,
|
|
560
|
+
filePath,
|
|
561
|
+
};
|
|
562
|
+
// Add to files map if exported
|
|
563
|
+
if (exportedName) {
|
|
564
|
+
inspectorState.cli.files.add(filePath);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
@@ -23,11 +23,12 @@ export const addFileExtendsCoreType = (node, checker, methods, expectedTypeName,
|
|
|
23
23
|
}
|
|
24
24
|
const variables = methods.get(fileName) || [];
|
|
25
25
|
if (!typeName) {
|
|
26
|
-
throw new Error('
|
|
26
|
+
throw new Error(`Found anonymous ${ts.isClassDeclaration(node) ? 'class' : 'interface'} extending ${expectedTypeName} in ${fileName}. ` +
|
|
27
|
+
`Classes and interfaces that extend core types must have a name.`);
|
|
27
28
|
}
|
|
28
29
|
variables.push({
|
|
29
30
|
variable: typeName,
|
|
30
|
-
type: typeName
|
|
31
|
+
type: typeName,
|
|
31
32
|
typePath: extendedTypeDeclarationPath,
|
|
32
33
|
});
|
|
33
34
|
methods.set(fileName, variables);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { PathToNameAndType } from '../types.js';
|
|
3
|
-
export declare const addFileWithFactory: (node: ts.Node, checker: ts.TypeChecker, methods: PathToNameAndType | undefined, expectedTypeName: string) => void;
|
|
2
|
+
import { PathToNameAndType, InspectorState } from '../types.js';
|
|
3
|
+
export declare const addFileWithFactory: (node: ts.Node, checker: ts.TypeChecker, methods: PathToNameAndType | undefined, expectedTypeName: string, state?: InspectorState) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
|
|
2
|
+
import { extractServicesFromFunction } from '../utils/extract-services.js';
|
|
3
|
+
export const addFileWithFactory = (node, checker, methods = new Map(), expectedTypeName, state) => {
|
|
3
4
|
if (ts.isVariableDeclaration(node)) {
|
|
4
5
|
const fileName = node.getSourceFile().fileName;
|
|
5
6
|
const variableTypeNode = node.type;
|
|
@@ -23,6 +24,22 @@ export const addFileWithFactory = (node, checker, methods = new Map(), expectedT
|
|
|
23
24
|
typePath: typeDeclarationPath,
|
|
24
25
|
});
|
|
25
26
|
methods.set(fileName, variables);
|
|
27
|
+
// Extract singleton services for CreateSessionServices factories
|
|
28
|
+
if (expectedTypeName === 'CreateSessionServices' &&
|
|
29
|
+
state &&
|
|
30
|
+
node.initializer) {
|
|
31
|
+
let functionNode;
|
|
32
|
+
if (ts.isArrowFunction(node.initializer)) {
|
|
33
|
+
functionNode = node.initializer;
|
|
34
|
+
}
|
|
35
|
+
else if (ts.isFunctionExpression(node.initializer)) {
|
|
36
|
+
functionNode = node.initializer;
|
|
37
|
+
}
|
|
38
|
+
if (functionNode) {
|
|
39
|
+
const servicesMeta = extractServicesFromFunction(functionNode);
|
|
40
|
+
state.sessionServicesMeta.set(variableName, servicesMeta.services);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
}
|
|
27
44
|
// Handle qualified type names if necessary
|
|
28
45
|
else if (ts.isQualifiedName(typeNameNode)) {
|
|
@@ -41,6 +58,22 @@ export const addFileWithFactory = (node, checker, methods = new Map(), expectedT
|
|
|
41
58
|
typePath: typeDeclarationPath,
|
|
42
59
|
});
|
|
43
60
|
methods.set(fileName, variables);
|
|
61
|
+
// Extract singleton services for CreateSessionServices factories
|
|
62
|
+
if (expectedTypeName === 'CreateSessionServices' &&
|
|
63
|
+
state &&
|
|
64
|
+
node.initializer) {
|
|
65
|
+
let functionNode;
|
|
66
|
+
if (ts.isArrowFunction(node.initializer)) {
|
|
67
|
+
functionNode = node.initializer;
|
|
68
|
+
}
|
|
69
|
+
else if (ts.isFunctionExpression(node.initializer)) {
|
|
70
|
+
functionNode = node.initializer;
|
|
71
|
+
}
|
|
72
|
+
if (functionNode) {
|
|
73
|
+
const servicesMeta = extractServicesFromFunction(functionNode);
|
|
74
|
+
state.sessionServicesMeta.set(variableName, servicesMeta.services);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
}
|
|
46
79
|
}
|