@pikku/inspector 0.10.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.11.0
2
+
3
+ ### Minor Changes
4
+
5
+ - Add workflow inspection and analysis
6
+ - Add enhanced type extraction utilities
7
+
8
+
1
9
  # @pikku/inspector
2
10
 
3
11
  ## 0.10.2
@@ -233,6 +233,7 @@ export const addFunctions = (logger, node, checker, state) => {
233
233
  const { pikkuFuncName, name, explicitName, exportedName } = extractFunctionName(node, checker, state.rootDir);
234
234
  let tags;
235
235
  let expose;
236
+ let internal;
236
237
  let docs;
237
238
  let objectNode;
238
239
  // determine the actual handler expression:
@@ -244,6 +245,7 @@ export const addFunctions = (logger, node, checker, state) => {
244
245
  objectNode = handlerNode;
245
246
  tags = getPropertyValue(handlerNode, 'tags') || undefined;
246
247
  expose = getPropertyValue(handlerNode, 'expose');
248
+ internal = getPropertyValue(handlerNode, 'internal');
247
249
  docs = getPropertyValue(handlerNode, 'docs');
248
250
  const fnProp = getPropertyAssignmentInitializer(handlerNode, 'func', true, checker);
249
251
  if (!fnProp ||
@@ -342,6 +344,7 @@ export const addFunctions = (logger, node, checker, state) => {
342
344
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
343
345
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
344
346
  expose: expose || undefined,
347
+ internal: internal || undefined,
345
348
  tags: tags || undefined,
346
349
  docs: docs || undefined,
347
350
  isDirectFunction,
@@ -363,6 +366,10 @@ export const addFunctions = (logger, node, checker, state) => {
363
366
  logger.error(`• Function with explicit name '${name}' is not exported, this is not allowed.`);
364
367
  return;
365
368
  }
369
+ // Mark internal functions as invoked to force bundling
370
+ if (internal) {
371
+ state.rpc.invokedFunctions.add(pikkuFuncName);
372
+ }
366
373
  if (expose) {
367
374
  state.rpc.exposedMeta[name] = pikkuFuncName;
368
375
  state.rpc.exposedFiles.set(name, {
@@ -376,8 +383,8 @@ export const addFunctions = (logger, node, checker, state) => {
376
383
  state.rpc.internalMeta[name] = pikkuFuncName;
377
384
  // But we only import the actual function if it's actually invoked to keep
378
385
  // bundle size down
379
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose) {
380
- state.rpc.internalFiles.set(name, {
386
+ if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
387
+ state.rpc.internalFiles.set(pikkuFuncName, {
381
388
  path: node.getSourceFile().fileName,
382
389
  exportedName,
383
390
  });
@@ -0,0 +1,6 @@
1
+ import { AddWiring } from '../types.js';
2
+ /**
3
+ * Inspector for wireWorkflow() calls
4
+ * Detects workflow registration and extracts metadata
5
+ */
6
+ export declare const addWorkflow: AddWiring;
@@ -0,0 +1,152 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue, getPropertyTags, } from '../utils/get-property-value.js';
3
+ import { extractFunctionName } from '../utils/extract-function-name.js';
4
+ import { getPropertyAssignmentInitializer, resolveFunctionDeclaration, } from '../utils/type-utils.js';
5
+ import { resolveMiddleware } from '../utils/middleware.js';
6
+ import { extractWireNames } from '../utils/post-process.js';
7
+ import { ErrorCode } from '../error-codes.js';
8
+ import { extractStringLiteral, extractNumberLiteral, extractPropertyString, isStringLike, isFunctionLike, } from '../utils/extract-node-value.js';
9
+ /**
10
+ * Scan for workflow.do() and workflow.sleep() calls to extract workflow steps
11
+ */
12
+ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
13
+ // Look for property access expressions: workflow.do or workflow.sleep
14
+ if (ts.isPropertyAccessExpression(node)) {
15
+ const { name } = node;
16
+ // Check if this is accessing 'do' or 'sleep' property
17
+ if (name.text === 'do' || name.text === 'sleep') {
18
+ // Check if the parent is a call expression
19
+ const parent = node.parent;
20
+ if (ts.isCallExpression(parent) && parent.expression === node) {
21
+ const args = parent.arguments;
22
+ if (name.text === 'do' && args.length >= 2) {
23
+ // workflow.do(stepName, rpcName|fn, data?, options?)
24
+ const stepNameArg = args[0];
25
+ const secondArg = args[1];
26
+ const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
27
+ const stepName = extractStringLiteral(stepNameArg, checker);
28
+ const description = extractDescription(optionsArg, checker) ?? undefined;
29
+ // Determine form by checking 2nd argument type
30
+ if (isStringLike(secondArg, checker)) {
31
+ // RPC form: workflow.do(stepName, rpcName, data, options?)
32
+ const rpcName = extractStringLiteral(secondArg, checker);
33
+ steps.push({
34
+ type: 'rpc',
35
+ stepName,
36
+ rpcName,
37
+ description,
38
+ });
39
+ state.rpc.invokedFunctions.add(rpcName);
40
+ }
41
+ else if (isFunctionLike(secondArg)) {
42
+ // Inline form: workflow.do(stepName, fn, options?)
43
+ steps.push({
44
+ type: 'inline',
45
+ stepName: stepName || '<dynamic>',
46
+ description: description || '<dynamic>',
47
+ });
48
+ }
49
+ }
50
+ else if (name.text === 'sleep' && args.length >= 2) {
51
+ // workflow.sleep(stepName, duration)
52
+ const stepNameArg = args[0];
53
+ const durationArg = args[1];
54
+ const stepName = extractStringLiteral(stepNameArg, checker);
55
+ const duration = extractDuration(durationArg, checker);
56
+ steps.push({
57
+ type: 'sleep',
58
+ stepName: stepName || '<dynamic>',
59
+ duration: duration || '<dynamic>',
60
+ });
61
+ }
62
+ }
63
+ }
64
+ }
65
+ // Don't recurse into nested functions - only look at top-level workflow calls
66
+ ts.forEachChild(node, (child) => {
67
+ if (ts.isFunctionDeclaration(child) ||
68
+ ts.isFunctionExpression(child) ||
69
+ ts.isArrowFunction(child)) {
70
+ return;
71
+ }
72
+ getWorkflowInvocations(child, checker, state, workflowName, steps);
73
+ });
74
+ }
75
+ /**
76
+ * Extract description from options object
77
+ */
78
+ function extractDescription(optionsNode, checker) {
79
+ if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
80
+ return null;
81
+ }
82
+ return extractPropertyString(optionsNode, 'description', checker);
83
+ }
84
+ /**
85
+ * Extract duration value (number or string)
86
+ */
87
+ function extractDuration(node, checker) {
88
+ const numValue = extractNumberLiteral(node);
89
+ if (numValue !== null) {
90
+ return numValue;
91
+ }
92
+ return extractStringLiteral(node, checker);
93
+ }
94
+ /**
95
+ * Inspector for wireWorkflow() calls
96
+ * Detects workflow registration and extracts metadata
97
+ */
98
+ export const addWorkflow = (logger, node, checker, state, options) => {
99
+ if (!ts.isCallExpression(node)) {
100
+ return;
101
+ }
102
+ const args = node.arguments;
103
+ const firstArg = args[0];
104
+ const expression = node.expression;
105
+ // Check if the call is to wireWorkflow
106
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireWorkflow') {
107
+ return;
108
+ }
109
+ if (!firstArg) {
110
+ return;
111
+ }
112
+ if (ts.isObjectLiteralExpression(firstArg)) {
113
+ const obj = firstArg;
114
+ const workflowName = getPropertyValue(obj, 'name');
115
+ const description = getPropertyValue(obj, 'description');
116
+ const docs = getPropertyValue(obj, 'docs') || undefined;
117
+ const tags = getPropertyTags(obj, 'Workflow', workflowName, logger);
118
+ // --- find the referenced function ---
119
+ const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
120
+ if (!workflowName) {
121
+ logger.critical(ErrorCode.MISSING_NAME, `Wasn't able to determine 'name' property for workflow wiring.`);
122
+ return;
123
+ }
124
+ if (!funcInitializer) {
125
+ logger.critical(ErrorCode.MISSING_FUNC, `No valid 'func' property for workflow '${workflowName}'.`);
126
+ return;
127
+ }
128
+ const pikkuFuncName = extractFunctionName(funcInitializer, checker, state.rootDir).pikkuFuncName;
129
+ // --- resolve middleware ---
130
+ const middleware = resolveMiddleware(state, obj, tags, checker);
131
+ // --- track used functions/middleware for service aggregation ---
132
+ state.serviceAggregation.usedFunctions.add(pikkuFuncName);
133
+ extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
134
+ state.workflows.files.add(node.getSourceFile().fileName);
135
+ // Extract workflow steps from function body
136
+ // Resolve the identifier to the actual function declaration
137
+ const resolvedFunc = resolveFunctionDeclaration(funcInitializer, checker);
138
+ const steps = [];
139
+ if (resolvedFunc) {
140
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
141
+ }
142
+ state.workflows.meta[workflowName] = {
143
+ pikkuFuncName,
144
+ workflowName,
145
+ description,
146
+ docs,
147
+ tags,
148
+ middleware,
149
+ steps,
150
+ };
151
+ }
152
+ };
@@ -17,6 +17,8 @@ export declare enum ErrorCode {
17
17
  MISSING_QUEUE_NAME = "PKU384",
18
18
  MISSING_CHANNEL_NAME = "PKU400",
19
19
  CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
20
+ DYNAMIC_STEP_NAME = "PKU529",
21
+ WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
20
22
  CONFIG_TYPE_NOT_FOUND = "PKU426",
21
23
  CONFIG_TYPE_UNDEFINED = "PKU427",
22
24
  SCHEMA_NO_ROOT = "PKU431",
@@ -31,5 +33,6 @@ export declare enum ErrorCode {
31
33
  PERMISSION_HANDLER_INVALID = "PKU835",
32
34
  PERMISSION_TAG_INVALID = "PKU836",
33
35
  PERMISSION_EMPTY_ARRAY = "PKU937",
34
- PERMISSION_PATTERN_INVALID = "PKU975"
36
+ PERMISSION_PATTERN_INVALID = "PKU975",
37
+ WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
35
38
  }
@@ -19,6 +19,8 @@ export var ErrorCode;
19
19
  ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
20
20
  ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
21
21
  ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
22
+ ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
23
+ ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
22
24
  // Configuration errors
23
25
  ErrorCode["CONFIG_TYPE_NOT_FOUND"] = "PKU426";
24
26
  ErrorCode["CONFIG_TYPE_UNDEFINED"] = "PKU427";
@@ -37,4 +39,6 @@ export var ErrorCode;
37
39
  ErrorCode["PERMISSION_TAG_INVALID"] = "PKU836";
38
40
  ErrorCode["PERMISSION_EMPTY_ARRAY"] = "PKU937";
39
41
  ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
42
+ // Feature Flag
43
+ ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
40
44
  })(ErrorCode || (ErrorCode = {}));
package/dist/inspector.js CHANGED
@@ -56,6 +56,10 @@ export function getInitialInspectorState(rootDir) {
56
56
  meta: {},
57
57
  files: new Set(),
58
58
  },
59
+ workflows: {
60
+ meta: {},
61
+ files: new Set(),
62
+ },
59
63
  rpc: {
60
64
  internalMeta: {},
61
65
  internalFiles: new Map(),
package/dist/types.d.ts CHANGED
@@ -2,7 +2,8 @@ import * as ts from 'typescript';
2
2
  import { ChannelsMeta } from '@pikku/core/channel';
3
3
  import { HTTPWiringsMeta } from '@pikku/core/http';
4
4
  import { ScheduledTasksMeta } from '@pikku/core/scheduler';
5
- import { queueWorkersMeta } from '@pikku/core/queue';
5
+ import { QueueWorkersMeta } from '@pikku/core/queue';
6
+ import { WorkflowsMeta } from '@pikku/core/workflow';
6
7
  import { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
7
8
  import { CLIMeta } from '@pikku/core/cli';
8
9
  import { TypesMap } from './types-map.js';
@@ -168,7 +169,11 @@ export interface InspectorState {
168
169
  files: Set<string>;
169
170
  };
170
171
  queueWorkers: {
171
- meta: queueWorkersMeta;
172
+ meta: QueueWorkersMeta;
173
+ files: Set<string>;
174
+ };
175
+ workflows: {
176
+ meta: WorkflowsMeta;
172
177
  files: Set<string>;
173
178
  };
174
179
  rpc: {
@@ -0,0 +1,24 @@
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 declare function extractStringLiteral(node: ts.Node, checker: ts.TypeChecker): string;
8
+ /**
9
+ * Check if node is string-like (string literal or template expression)
10
+ */
11
+ export declare function isStringLike(node: ts.Node, _checker: ts.TypeChecker): boolean;
12
+ /**
13
+ * Check if node is function-like (arrow, function expression, or function declaration)
14
+ */
15
+ export declare function isFunctionLike(node: ts.Node): boolean;
16
+ /**
17
+ * Extract number literal value from a node
18
+ */
19
+ export declare function extractNumberLiteral(node: ts.Node): number | null;
20
+ /**
21
+ * Extract a property value from an object literal expression
22
+ * Returns the extracted value or null if not found/cannot extract
23
+ */
24
+ export declare function extractPropertyString(objNode: ts.ObjectLiteralExpression, propertyName: string, checker: ts.TypeChecker): string | null;
@@ -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
+ }
@@ -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, {
@@ -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()),
@@ -137,6 +141,10 @@ export function deserializeInspectorState(data) {
137
141
  meta: data.queueWorkers.meta,
138
142
  files: new Set(data.queueWorkers.files),
139
143
  },
144
+ workflows: {
145
+ meta: data.workflows.meta,
146
+ files: new Set(data.workflows.files),
147
+ },
140
148
  rpc: {
141
149
  internalMeta: data.rpc.internalMeta,
142
150
  internalFiles: new Map(data.rpc.internalFiles),
@@ -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: () => {}
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.2",
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.2",
19
+ "@pikku/core": "^0.11.0",
20
20
  "path-to-regexp": "^8.3.0",
21
21
  "typescript": "^5.9"
22
22
  },
@@ -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
  })