@pikku/inspector 0.10.1 → 0.11.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/add/add-channel.js +68 -14
  3. package/dist/add/add-functions.js +9 -2
  4. package/dist/add/add-workflow.d.ts +6 -0
  5. package/dist/add/add-workflow.js +152 -0
  6. package/dist/error-codes.d.ts +4 -1
  7. package/dist/error-codes.js +4 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/inspector.d.ts +6 -0
  11. package/dist/inspector.js +53 -15
  12. package/dist/types.d.ts +10 -2
  13. package/dist/utils/extract-node-value.d.ts +24 -0
  14. package/dist/utils/extract-node-value.js +79 -0
  15. package/dist/utils/post-process.d.ts +1 -1
  16. package/dist/utils/post-process.js +30 -0
  17. package/dist/utils/serialize-inspector-state.d.ts +6 -0
  18. package/dist/utils/serialize-inspector-state.js +12 -0
  19. package/dist/utils/type-utils.d.ts +4 -0
  20. package/dist/utils/type-utils.js +60 -3
  21. package/dist/visit.js +2 -0
  22. package/package.json +2 -2
  23. package/src/add/add-channel.ts +94 -19
  24. package/src/add/add-functions.ts +10 -2
  25. package/src/add/add-workflow.ts +231 -0
  26. package/src/error-codes.ts +5 -0
  27. package/src/index.ts +1 -1
  28. package/src/inspector.ts +77 -22
  29. package/src/types.ts +10 -2
  30. package/src/utils/extract-node-value.ts +101 -0
  31. package/src/utils/post-process.ts +40 -2
  32. package/src/utils/serialize-inspector-state.ts +18 -0
  33. package/src/utils/test-data/inspector-state.json +4 -0
  34. package/src/utils/type-utils.ts +74 -3
  35. package/src/visit.ts +3 -1
  36. package/tsconfig.tsbuildinfo +1 -1
  37. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  38. package/src/add/add-mcp-resource.ts.tmp +0 -0
@@ -0,0 +1,79 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Extract string literal value from a TypeScript node.
4
+ * Handles string literals, template literals (including placeholders),
5
+ * and constant variable references.
6
+ */
7
+ export function extractStringLiteral(node, checker) {
8
+ if (ts.isStringLiteral(node)) {
9
+ return node.text;
10
+ }
11
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
12
+ return node.text;
13
+ }
14
+ if (ts.isTemplateExpression(node)) {
15
+ // reconstruct: `head + ${expr} + middle + ${expr} + tail`
16
+ let result = node.head.text;
17
+ for (const span of node.templateSpans) {
18
+ const exprText = span.expression.getText();
19
+ result += '${' + exprText + '}' + span.literal.text;
20
+ }
21
+ return result;
22
+ }
23
+ // Try to evaluate constant identifiers
24
+ if (ts.isIdentifier(node)) {
25
+ const symbol = checker.getSymbolAtLocation(node);
26
+ if (symbol?.valueDeclaration &&
27
+ ts.isVariableDeclaration(symbol.valueDeclaration)) {
28
+ const init = symbol.valueDeclaration.initializer;
29
+ if (init) {
30
+ return extractStringLiteral(init, checker);
31
+ }
32
+ }
33
+ }
34
+ throw new Error('Unable to extract string literal from node');
35
+ }
36
+ /**
37
+ * Check if node is string-like (string literal or template expression)
38
+ */
39
+ export function isStringLike(node, _checker) {
40
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
41
+ return true;
42
+ }
43
+ // Check if it's a template string with substitutions
44
+ if (ts.isTemplateExpression(node)) {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ /**
50
+ * Check if node is function-like (arrow, function expression, or function declaration)
51
+ */
52
+ export function isFunctionLike(node) {
53
+ return (ts.isArrowFunction(node) ||
54
+ ts.isFunctionExpression(node) ||
55
+ ts.isFunctionDeclaration(node));
56
+ }
57
+ /**
58
+ * Extract number literal value from a node
59
+ */
60
+ export function extractNumberLiteral(node) {
61
+ if (ts.isNumericLiteral(node)) {
62
+ return Number(node.text);
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Extract a property value from an object literal expression
68
+ * Returns the extracted value or null if not found/cannot extract
69
+ */
70
+ export function extractPropertyString(objNode, propertyName, checker) {
71
+ for (const prop of objNode.properties) {
72
+ if (ts.isPropertyAssignment(prop) &&
73
+ ts.isIdentifier(prop.name) &&
74
+ prop.name.text === propertyName) {
75
+ return extractStringLiteral(prop.initializer, checker);
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -13,4 +13,4 @@ export declare function extractWireNames(list?: MiddlewareMetadata[] | Permissio
13
13
  * Note: usedFunctions, usedMiddleware, and usedPermissions are tracked directly
14
14
  * in the add-* methods during AST traversal for efficiency.
15
15
  */
16
- export declare function aggregateRequiredServices(state: InspectorState): void;
16
+ export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
@@ -1,3 +1,4 @@
1
+ import { extractTypeKeys } from './type-utils.js';
1
2
  /**
2
3
  * Helper to extract wire-level middleware/permission names from metadata.
3
4
  * Only extracts type:'wire' variants (individual middleware/permissions).
@@ -39,6 +40,33 @@ function expandAndAddGroupServices(list, state, addServices, isMiddleware) {
39
40
  }
40
41
  }
41
42
  }
43
+ /**
44
+ * Extracts all service names from SingletonServices and Services types.
45
+ * This provides the complete list of available services for code generation.
46
+ * Only runs if typesLookup is available (omitted in deserialized states).
47
+ */
48
+ function extractAllServices(state) {
49
+ // Skip if typesLookup is not available (e.g., deserialized state)
50
+ if (!('typesLookup' in state)) {
51
+ return;
52
+ }
53
+ // Extract all singleton services from the SingletonServices type
54
+ const singletonServicesTypes = state.typesLookup.get('SingletonServices');
55
+ if (singletonServicesTypes && singletonServicesTypes.length > 0) {
56
+ const singletonServiceNames = extractTypeKeys(singletonServicesTypes[0]);
57
+ state.serviceAggregation.allSingletonServices = singletonServiceNames.sort();
58
+ }
59
+ // Extract all services from the Services type
60
+ const servicesTypes = state.typesLookup.get('Services');
61
+ if (servicesTypes && servicesTypes.length > 0) {
62
+ const allServiceNames = extractTypeKeys(servicesTypes[0]);
63
+ // Session services are those in Services but not in SingletonServices
64
+ const singletonSet = new Set(state.serviceAggregation.allSingletonServices);
65
+ state.serviceAggregation.allSessionServices = allServiceNames
66
+ .filter((name) => !singletonSet.has(name))
67
+ .sort();
68
+ }
69
+ }
42
70
  /**
43
71
  * Aggregates all required services from wired functions, middleware, and permissions.
44
72
  * Must be called after AST traversal completes.
@@ -47,6 +75,8 @@ function expandAndAddGroupServices(list, state, addServices, isMiddleware) {
47
75
  * in the add-* methods during AST traversal for efficiency.
48
76
  */
49
77
  export function aggregateRequiredServices(state) {
78
+ // First, extract all available services from types
79
+ extractAllServices(state);
50
80
  const { requiredServices, usedFunctions, usedMiddleware, usedPermissions } = state.serviceAggregation;
51
81
  // Internal services (always excluded from tree-shaking)
52
82
  const internalServices = new Set(['rpc', 'mcp', 'channel', 'userSession']);
@@ -123,6 +123,10 @@ export interface SerializableInspectorState {
123
123
  meta: InspectorState['queueWorkers']['meta'];
124
124
  files: string[];
125
125
  };
126
+ workflows: {
127
+ meta: InspectorState['workflows']['meta'];
128
+ files: string[];
129
+ };
126
130
  rpc: {
127
131
  internalMeta: InspectorState['rpc']['internalMeta'];
128
132
  internalFiles: Array<[string, {
@@ -165,6 +169,8 @@ export interface SerializableInspectorState {
165
169
  usedFunctions: string[];
166
170
  usedMiddleware: string[];
167
171
  usedPermissions: string[];
172
+ allSingletonServices: string[];
173
+ allSessionServices: string[];
168
174
  };
169
175
  }
170
176
  /**
@@ -50,6 +50,10 @@ export function serializeInspectorState(state) {
50
50
  meta: state.queueWorkers.meta,
51
51
  files: Array.from(state.queueWorkers.files),
52
52
  },
53
+ workflows: {
54
+ meta: state.workflows.meta,
55
+ files: Array.from(state.workflows.files),
56
+ },
53
57
  rpc: {
54
58
  internalMeta: state.rpc.internalMeta,
55
59
  internalFiles: Array.from(state.rpc.internalFiles.entries()),
@@ -80,6 +84,8 @@ export function serializeInspectorState(state) {
80
84
  usedFunctions: Array.from(state.serviceAggregation.usedFunctions),
81
85
  usedMiddleware: Array.from(state.serviceAggregation.usedMiddleware),
82
86
  usedPermissions: Array.from(state.serviceAggregation.usedPermissions),
87
+ allSingletonServices: state.serviceAggregation.allSingletonServices,
88
+ allSessionServices: state.serviceAggregation.allSessionServices,
83
89
  },
84
90
  };
85
91
  }
@@ -135,6 +141,10 @@ export function deserializeInspectorState(data) {
135
141
  meta: data.queueWorkers.meta,
136
142
  files: new Set(data.queueWorkers.files),
137
143
  },
144
+ workflows: {
145
+ meta: data.workflows.meta,
146
+ files: new Set(data.workflows.files),
147
+ },
138
148
  rpc: {
139
149
  internalMeta: data.rpc.internalMeta,
140
150
  internalFiles: new Map(data.rpc.internalFiles),
@@ -165,6 +175,8 @@ export function deserializeInspectorState(data) {
165
175
  usedFunctions: new Set(data.serviceAggregation.usedFunctions),
166
176
  usedMiddleware: new Set(data.serviceAggregation.usedMiddleware),
167
177
  usedPermissions: new Set(data.serviceAggregation.usedPermissions),
178
+ allSingletonServices: data.serviceAggregation.allSingletonServices,
179
+ allSessionServices: data.serviceAggregation.allSessionServices,
168
180
  },
169
181
  };
170
182
  }
@@ -1,3 +1,7 @@
1
1
  import * as ts from 'typescript';
2
2
  export declare const extractTypeKeys: (type: ts.Type) => string[];
3
+ /**
4
+ * Resolve an identifier or call expression to the actual function declaration
5
+ */
6
+ export declare function resolveFunctionDeclaration(node: ts.Node, checker: ts.TypeChecker): ts.Node | null;
3
7
  export declare function getPropertyAssignmentInitializer(obj: ts.ObjectLiteralExpression, propName: string, followShorthand?: boolean, checker?: ts.TypeChecker): ts.Expression | undefined;
@@ -2,6 +2,61 @@ import * as ts from 'typescript';
2
2
  export const extractTypeKeys = (type) => {
3
3
  return type.getProperties().map((symbol) => symbol.getName());
4
4
  };
5
+ /**
6
+ * Resolve an identifier or call expression to the actual function declaration
7
+ */
8
+ export function resolveFunctionDeclaration(node, checker) {
9
+ // If it's already a function-like node, return it
10
+ if (ts.isFunctionDeclaration(node) ||
11
+ ts.isFunctionExpression(node) ||
12
+ ts.isArrowFunction(node)) {
13
+ return node;
14
+ }
15
+ // If it's a call expression (e.g., pikkuWorkflowFunc(...)), get its first argument
16
+ if (ts.isCallExpression(node) && node.arguments.length > 0) {
17
+ const firstArg = node.arguments[0];
18
+ if (ts.isFunctionExpression(firstArg) || ts.isArrowFunction(firstArg)) {
19
+ return firstArg;
20
+ }
21
+ }
22
+ // If it's an identifier, resolve to declaration
23
+ if (ts.isIdentifier(node)) {
24
+ const symbol = checker.getSymbolAtLocation(node);
25
+ if (!symbol)
26
+ return null;
27
+ // Try valueDeclaration first, then fallback to declarations[0]
28
+ const decl = symbol.valueDeclaration || symbol.declarations?.[0];
29
+ if (!decl)
30
+ return null;
31
+ // If it's an import specifier, resolve the aliased symbol
32
+ if (ts.isImportSpecifier(decl)) {
33
+ const aliasedSymbol = checker.getAliasedSymbol(symbol);
34
+ if (aliasedSymbol) {
35
+ const aliasedDecl = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
36
+ if (aliasedDecl) {
37
+ // For variable declarations, get the initializer
38
+ if (ts.isVariableDeclaration(aliasedDecl) &&
39
+ aliasedDecl.initializer) {
40
+ return resolveFunctionDeclaration(aliasedDecl.initializer, checker);
41
+ }
42
+ // For function declarations, return directly
43
+ if (ts.isFunctionDeclaration(aliasedDecl)) {
44
+ return aliasedDecl;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ // If it's a variable declaration, get the initializer
50
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
51
+ return resolveFunctionDeclaration(decl.initializer, checker);
52
+ }
53
+ // If it's a function declaration
54
+ if (ts.isFunctionDeclaration(decl)) {
55
+ return decl;
56
+ }
57
+ }
58
+ return null;
59
+ }
5
60
  export function getPropertyAssignmentInitializer(obj, propName, followShorthand = false, checker) {
6
61
  for (const prop of obj.properties) {
7
62
  // ① foo: () => {}
@@ -22,17 +77,19 @@ export function getPropertyAssignmentInitializer(obj, propName, followShorthand
22
77
  prop.name.text === propName) {
23
78
  if (!checker)
24
79
  return prop.name; // best effort without a checker
25
- let sym = checker.getSymbolAtLocation(prop.name);
80
+ // Use the proper TypeScript API for shorthand property resolution
81
+ let sym = checker.getShorthandAssignmentValueSymbol(prop);
26
82
  if (sym && sym.flags & ts.SymbolFlags.Alias) {
27
83
  sym = checker.getAliasedSymbol(sym);
28
84
  }
29
85
  const decl = sym?.declarations?.[0];
30
- // const foo = () => {}
86
+ // const foo = () => {} or const foo = pikkuFunc(...)
31
87
  if (decl &&
32
88
  ts.isVariableDeclaration(decl) &&
33
89
  decl.initializer &&
34
90
  (ts.isArrowFunction(decl.initializer) ||
35
- ts.isFunctionExpression(decl.initializer))) {
91
+ ts.isFunctionExpression(decl.initializer) ||
92
+ ts.isCallExpression(decl.initializer))) {
36
93
  return decl.initializer;
37
94
  }
38
95
  // function foo() {}
package/dist/visit.js CHANGED
@@ -4,6 +4,7 @@ import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
4
4
  import { addHTTPRoute } from './add/add-http-route.js';
5
5
  import { addSchedule } from './add/add-schedule.js';
6
6
  import { addQueueWorker } from './add/add-queue-worker.js';
7
+ import { addWorkflow } from './add/add-workflow.js';
7
8
  import { addMCPResource } from './add/add-mcp-resource.js';
8
9
  import { addMCPTool } from './add/add-mcp-tool.js';
9
10
  import { addMCPPrompt } from './add/add-mcp-prompt.js';
@@ -24,6 +25,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
24
25
  addRPCInvocations(node, state, logger);
25
26
  addMiddleware(logger, node, checker, state, options);
26
27
  addPermission(logger, node, checker, state, options);
28
+ addWorkflow(logger, node, checker, state, options);
27
29
  ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
28
30
  };
29
31
  export const visitRoutes = (logger, checker, node, state, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "test:coverage": "bash run-tests.sh --coverage"
17
17
  },
18
18
  "dependencies": {
19
- "@pikku/core": "^0.10.1",
19
+ "@pikku/core": "^0.11.0",
20
20
  "path-to-regexp": "^8.3.0",
21
21
  "typescript": "^5.9"
22
22
  },
@@ -136,7 +136,15 @@ export function addMessagesRoutes(
136
136
  const init = getInitializerOf(routeElem)
137
137
  if (!init) continue
138
138
 
139
- const routeKey = routeElem.name!.getText()
139
+ // Get the route key, stripping quotes if it's a string literal
140
+ const routeName = routeElem.name
141
+ if (!routeName) continue
142
+
143
+ let routeKey = routeName.getText()
144
+ // For string literals like 'greet' or "greet", strip the quotes
145
+ if (ts.isStringLiteral(routeName)) {
146
+ routeKey = routeName.text
147
+ }
140
148
 
141
149
  // For shorthand properties, we need to resolve the identifier to its declaration
142
150
  if (ts.isShorthandPropertyAssignment(routeElem)) {
@@ -196,8 +204,17 @@ export function addMessagesRoutes(
196
204
  // Look up in the registry
197
205
  const fnMeta = state.functions.meta[handlerName]
198
206
  if (fnMeta) {
207
+ // Resolve middleware for this route
208
+ const routeTags = ts.isObjectLiteralExpression(init)
209
+ ? getPropertyTags(init, 'channel', channelKey, logger)
210
+ : undefined
211
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
212
+ ? resolveMiddleware(state, init, routeTags, checker)
213
+ : undefined
214
+
199
215
  result[channelKey]![routeKey] = {
200
216
  pikkuFuncName: handlerName,
217
+ middleware: routeMiddleware,
201
218
  }
202
219
  continue
203
220
  }
@@ -214,8 +231,17 @@ export function addMessagesRoutes(
214
231
  // Look up in the registry
215
232
  const fnMeta = state.functions.meta[handlerName]
216
233
  if (fnMeta) {
234
+ // Resolve middleware for this route
235
+ const routeTags = ts.isObjectLiteralExpression(init)
236
+ ? getPropertyTags(init, 'channel', channelKey, logger)
237
+ : undefined
238
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
239
+ ? resolveMiddleware(state, init, routeTags, checker)
240
+ : undefined
241
+
217
242
  result[channelKey]![routeKey] = {
218
243
  pikkuFuncName: handlerName,
244
+ middleware: routeMiddleware,
219
245
  }
220
246
  continue
221
247
  }
@@ -249,8 +275,24 @@ export function addMessagesRoutes(
249
275
 
250
276
  const fnMeta = state.functions.meta[handlerName]
251
277
  if (fnMeta) {
278
+ // Resolve middleware for this route
279
+ const routeTags = ts.isObjectLiteralExpression(init)
280
+ ? getPropertyTags(
281
+ init,
282
+ 'channel',
283
+ channelKey,
284
+ logger
285
+ )
286
+ : undefined
287
+ const routeMiddleware = ts.isObjectLiteralExpression(
288
+ init
289
+ )
290
+ ? resolveMiddleware(state, init, routeTags, checker)
291
+ : undefined
292
+
252
293
  result[channelKey]![routeKey] = {
253
294
  pikkuFuncName: handlerName,
295
+ middleware: routeMiddleware,
254
296
  }
255
297
  continue
256
298
  }
@@ -264,8 +306,24 @@ export function addMessagesRoutes(
264
306
 
265
307
  const fnMeta = state.functions.meta[handlerName]
266
308
  if (fnMeta) {
309
+ // Resolve middleware for this route
310
+ const routeTags = ts.isObjectLiteralExpression(init)
311
+ ? getPropertyTags(
312
+ init,
313
+ 'channel',
314
+ channelKey,
315
+ logger
316
+ )
317
+ : undefined
318
+ const routeMiddleware = ts.isObjectLiteralExpression(
319
+ init
320
+ )
321
+ ? resolveMiddleware(state, init, routeTags, checker)
322
+ : undefined
323
+
267
324
  result[channelKey]![routeKey] = {
268
325
  pikkuFuncName: handlerName,
326
+ middleware: routeMiddleware,
269
327
  }
270
328
  continue
271
329
  }
@@ -336,8 +394,17 @@ export function addMessagesRoutes(
336
394
  const fnMeta = state.functions.meta[handlerName]
337
395
 
338
396
  if (fnMeta) {
397
+ // Resolve middleware for this route
398
+ const routeTags = ts.isObjectLiteralExpression(init)
399
+ ? getPropertyTags(init, 'channel', channelKey, logger)
400
+ : undefined
401
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
402
+ ? resolveMiddleware(state, init, routeTags, checker)
403
+ : undefined
404
+
339
405
  result[channelKey]![routeKey] = {
340
406
  pikkuFuncName: handlerName,
407
+ middleware: routeMiddleware,
341
408
  }
342
409
  continue // Skip the normal processing below
343
410
  }
@@ -368,8 +435,18 @@ export function addMessagesRoutes(
368
435
  continue
369
436
  }
370
437
 
438
+ // Resolve middleware and permissions for this route
439
+ // Check if the route config is an object literal with middleware/permissions
440
+ const routeTags = ts.isObjectLiteralExpression(init)
441
+ ? getPropertyTags(init, 'channel', channelKey, logger)
442
+ : undefined
443
+ const routeMiddleware = ts.isObjectLiteralExpression(init)
444
+ ? resolveMiddleware(state, init, routeTags, checker)
445
+ : undefined
446
+
371
447
  result[channelKey]![routeKey] = {
372
448
  pikkuFuncName: handlerName,
449
+ middleware: routeMiddleware,
373
450
  }
374
451
  }
375
452
  }
@@ -417,13 +494,13 @@ export const addChannel: AddWiring = (
417
494
  const connect = getPropertyAssignmentInitializer(
418
495
  obj,
419
496
  'onConnect',
420
- false,
497
+ true,
421
498
  checker
422
499
  )
423
500
  const disconnect = getPropertyAssignmentInitializer(
424
501
  obj,
425
502
  'onDisconnect',
426
- false,
503
+ true,
427
504
  checker
428
505
  )
429
506
 
@@ -432,28 +509,26 @@ export const addChannel: AddWiring = (
432
509
  const onMsgProp = getPropertyAssignmentInitializer(
433
510
  obj,
434
511
  'onMessage',
435
- false,
512
+ true,
436
513
  checker
437
514
  )
438
515
 
439
516
  if (onMsgProp) {
440
- const handlerName =
441
- onMsgProp &&
442
- getHandlerNameFromExpression(onMsgProp, checker, state.rootDir)
443
- const fnMeta = handlerName && state.functions.meta[handlerName]
517
+ const { pikkuFuncName } = extractFunctionName(
518
+ onMsgProp,
519
+ checker,
520
+ state.rootDir
521
+ )
522
+ const fnMeta = state.functions.meta[pikkuFuncName]
444
523
  if (!fnMeta) {
445
- console.error(
446
- `No function metadata for onMessage handler '${handlerName}'`
524
+ logger.critical(
525
+ ErrorCode.FUNCTION_METADATA_NOT_FOUND,
526
+ `No function metadata found for onMessage handler '${pikkuFuncName}'`
447
527
  )
448
- throw new Error()
449
- } else {
450
- message = {
451
- pikkuFuncName: extractFunctionName(
452
- onMsgProp as any,
453
- checker,
454
- state.rootDir
455
- ).pikkuFuncName,
456
- }
528
+ return
529
+ }
530
+ message = {
531
+ pikkuFuncName,
457
532
  }
458
533
  }
459
534
 
@@ -305,6 +305,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
305
305
 
306
306
  let tags: string[] | undefined
307
307
  let expose: boolean | undefined
308
+ let internal: boolean | undefined
308
309
  let docs: PikkuDocs | undefined
309
310
  let objectNode: ts.ObjectLiteralExpression | undefined
310
311
 
@@ -318,6 +319,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
318
319
  objectNode = handlerNode
319
320
  tags = (getPropertyValue(handlerNode, 'tags') as string[]) || undefined
320
321
  expose = getPropertyValue(handlerNode, 'expose') as boolean | undefined
322
+ internal = getPropertyValue(handlerNode, 'internal') as boolean | undefined
321
323
  docs = getPropertyValue(handlerNode, 'docs') as PikkuDocs | undefined
322
324
 
323
325
  const fnProp = getPropertyAssignmentInitializer(
@@ -454,6 +456,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
454
456
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
455
457
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
456
458
  expose: expose || undefined,
459
+ internal: internal || undefined,
457
460
  tags: tags || undefined,
458
461
  docs: docs || undefined,
459
462
  isDirectFunction,
@@ -481,6 +484,11 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
481
484
  return
482
485
  }
483
486
 
487
+ // Mark internal functions as invoked to force bundling
488
+ if (internal) {
489
+ state.rpc.invokedFunctions.add(pikkuFuncName)
490
+ }
491
+
484
492
  if (expose) {
485
493
  state.rpc.exposedMeta[name] = pikkuFuncName
486
494
  state.rpc.exposedFiles.set(name, {
@@ -496,8 +504,8 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
496
504
 
497
505
  // But we only import the actual function if it's actually invoked to keep
498
506
  // bundle size down
499
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose) {
500
- state.rpc.internalFiles.set(name, {
507
+ if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
508
+ state.rpc.internalFiles.set(pikkuFuncName, {
501
509
  path: node.getSourceFile().fileName,
502
510
  exportedName,
503
511
  })