@pikku/inspector 0.12.22 → 0.12.24

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 (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/add/add-addon-bans.d.ts +7 -0
  3. package/dist/add/add-addon-bans.js +65 -0
  4. package/dist/add/add-auth.js +43 -0
  5. package/dist/add/add-channel.js +47 -6
  6. package/dist/add/add-cli.js +17 -0
  7. package/dist/add/add-http-route.d.ts +11 -1
  8. package/dist/add/add-http-route.js +37 -0
  9. package/dist/add/add-http-routes.d.ts +0 -3
  10. package/dist/add/add-http-routes.js +179 -36
  11. package/dist/error-codes.d.ts +3 -1
  12. package/dist/error-codes.js +3 -0
  13. package/dist/inspector.js +17 -5
  14. package/dist/types.d.ts +48 -1
  15. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  16. package/dist/utils/get-exported-variable-name.js +34 -0
  17. package/dist/utils/load-addon-functions-meta.js +98 -0
  18. package/dist/utils/post-process.js +16 -3
  19. package/dist/utils/resolve-addon-package.js +3 -1
  20. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  21. package/dist/utils/resolve-ref-contract.js +46 -0
  22. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  23. package/dist/utils/serialize-inspector-state.js +9 -0
  24. package/dist/visit.js +24 -19
  25. package/package.json +1 -1
  26. package/src/add/add-addon-bans.ts +84 -0
  27. package/src/add/add-auth.test.ts +94 -0
  28. package/src/add/add-auth.ts +46 -0
  29. package/src/add/add-channel.ts +66 -7
  30. package/src/add/add-cli.ts +30 -0
  31. package/src/add/add-http-route.ts +75 -1
  32. package/src/add/add-http-routes.ts +283 -41
  33. package/src/add/addon-bans.test.ts +121 -0
  34. package/src/add/addon-contracts.test.ts +221 -0
  35. package/src/error-codes.ts +4 -0
  36. package/src/inspector.ts +17 -5
  37. package/src/types.ts +70 -1
  38. package/src/utils/get-exported-variable-name.ts +48 -0
  39. package/src/utils/load-addon-functions-meta.ts +164 -0
  40. package/src/utils/post-process.ts +17 -3
  41. package/src/utils/resolve-addon-package.ts +6 -1
  42. package/src/utils/resolve-ref-contract.ts +71 -0
  43. package/src/utils/serialize-inspector-state.ts +10 -0
  44. package/src/visit.ts +26 -19
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -1,31 +1,40 @@
1
1
  import * as ts from 'typescript';
2
2
  import { getPropertyValue } from '../utils/get-property-value.js';
3
- import { registerHTTPRoute } from './add-http-route.js';
3
+ import { registerHTTPRoute, registerHTTPRouteMeta } from './add-http-route.js';
4
4
  import { resolveIdentifier } from '../utils/resolve-identifier.js';
5
- /**
6
- * Process wireHTTPRoutes calls
7
- */
8
- export const addHTTPRoutes = (logger, node, checker, state, _options) => {
5
+ import { extractFunctionName } from '../utils/extract-function-name.js';
6
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
7
+ import { resolveAddonName } from '../utils/resolve-addon-package.js';
8
+ import { resolveRefContract, } from '../utils/resolve-ref-contract.js';
9
+ import { getExportedVariableName } from '../utils/get-exported-variable-name.js';
10
+ export const addHTTPRoutes = (logger, node, checker, state, options) => {
9
11
  if (!ts.isCallExpression(node))
10
12
  return;
11
13
  const { expression, arguments: args } = node;
12
- if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTPRoutes')
14
+ if (!ts.isIdentifier(expression))
15
+ return;
16
+ if (expression.text === 'defineHTTPRoutes') {
17
+ const exportName = getExportedVariableName(node, options.sourceFile);
18
+ const firstArg = args[0];
19
+ if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
20
+ const contract = serializeHTTPRoutesContract(firstArg, checker, state);
21
+ if (contract) {
22
+ state.exportedContracts.http[exportName] = contract;
23
+ }
24
+ }
25
+ return;
26
+ }
27
+ if (expression.text !== 'wireHTTPRoutes')
13
28
  return;
14
29
  const firstArg = args[0];
15
30
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
16
31
  return;
17
- // Extract group config
18
32
  const groupConfig = extractGroupConfig(firstArg);
19
- // Get routes property
20
33
  const routesProp = getPropertyAssignment(firstArg, 'routes');
21
34
  if (!routesProp)
22
35
  return;
23
- // Process routes recursively
24
36
  processRoutes(routesProp.initializer, groupConfig, state, checker, logger, node.getSourceFile());
25
37
  };
26
- /**
27
- * Get a property assignment from an object literal
28
- */
29
38
  function getPropertyAssignment(obj, propName) {
30
39
  for (const prop of obj.properties) {
31
40
  if (ts.isPropertyAssignment(prop) &&
@@ -36,9 +45,6 @@ function getPropertyAssignment(obj, propName) {
36
45
  }
37
46
  return undefined;
38
47
  }
39
- /**
40
- * Extract group configuration from an object literal
41
- */
42
48
  function extractGroupConfig(obj) {
43
49
  const basePath = getPropertyValue(obj, 'basePath') || '';
44
50
  const tags = getPropertyValue(obj, 'tags') || [];
@@ -49,9 +55,6 @@ function extractGroupConfig(obj) {
49
55
  auth: auth === true ? true : auth === false ? false : undefined,
50
56
  };
51
57
  }
52
- /**
53
- * Merge two group configs following cascading rules
54
- */
55
58
  function mergeConfigs(parent, child) {
56
59
  return {
57
60
  basePath: parent.basePath + child.basePath,
@@ -59,9 +62,6 @@ function mergeConfigs(parent, child) {
59
62
  auth: child.auth ?? parent.auth,
60
63
  };
61
64
  }
62
- /**
63
- * Check if a value is a route config (has method, func, and route)
64
- */
65
65
  function isRouteConfig(obj) {
66
66
  let hasMethod = false;
67
67
  let hasFunc = false;
@@ -78,9 +78,6 @@ function isRouteConfig(obj) {
78
78
  }
79
79
  return hasMethod && hasFunc && hasRoute;
80
80
  }
81
- /**
82
- * Check if a value is a route contract (has routes property but no method/func)
83
- */
84
81
  function isRouteContract(obj) {
85
82
  let hasRoutes = false;
86
83
  let hasMethod = false;
@@ -97,11 +94,7 @@ function isRouteContract(obj) {
97
94
  }
98
95
  return hasRoutes && !hasMethod && !hasFunc;
99
96
  }
100
- /**
101
- * Recursively process routes - handles nested maps, contracts, and identifiers
102
- */
103
97
  function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
104
- // Handle array of routes
105
98
  if (ts.isArrayLiteralExpression(node)) {
106
99
  for (const element of node.elements) {
107
100
  if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
@@ -110,14 +103,11 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
110
103
  }
111
104
  return;
112
105
  }
113
- // Handle object literal
114
106
  if (ts.isObjectLiteralExpression(node)) {
115
- // Check if this is a route config
116
107
  if (isRouteConfig(node)) {
117
108
  processRoute(node, parentConfig, state, checker, logger, sourceFile);
118
109
  return;
119
110
  }
120
- // Check if this is a route contract
121
111
  if (isRouteContract(node)) {
122
112
  const contractConfig = extractGroupConfig(node);
123
113
  const mergedConfig = mergeConfigs(parentConfig, contractConfig);
@@ -127,15 +117,25 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
127
117
  }
128
118
  return;
129
119
  }
130
- // Otherwise it's a nested map - process each property
131
120
  for (const prop of node.properties) {
132
121
  if (ts.isPropertyAssignment(prop)) {
122
+ const ref = resolveRefContract(prop.initializer, 'refHTTP', state.exportedContracts.addonHttp);
123
+ if (ref) {
124
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile);
125
+ continue;
126
+ }
133
127
  processRoutes(prop.initializer, parentConfig, state, checker, logger, sourceFile);
134
128
  }
135
129
  }
136
130
  return;
137
131
  }
138
- // Handle identifier - resolve to its definition
132
+ if (ts.isCallExpression(node)) {
133
+ const ref = resolveRefContract(node, 'refHTTP', state.exportedContracts.addonHttp);
134
+ if (ref) {
135
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile);
136
+ }
137
+ return;
138
+ }
139
139
  if (ts.isIdentifier(node)) {
140
140
  const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes']);
141
141
  if (resolved) {
@@ -143,9 +143,152 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
143
143
  }
144
144
  }
145
145
  }
146
- /**
147
- * Register a single route using the shared registerHTTPRoute function
148
- */
146
+ function processRefHTTPContract(ref, parentConfig, state, logger, sourceFile) {
147
+ const basePath = ref.basePath !== undefined ? ref.basePath : ref.contract.basePath || '';
148
+ processExportedRouteMap(ref.contract.routes, mergeConfigs(parentConfig, {
149
+ basePath,
150
+ tags: ref.contract.tags || [],
151
+ auth: ref.contract.auth,
152
+ }), state, logger, sourceFile);
153
+ }
154
+ function processExportedRouteMap(routes, parentConfig, state, logger, sourceFile) {
155
+ for (const value of Object.values(routes)) {
156
+ if (isExportedRouteConfig(value)) {
157
+ registerHTTPRouteMeta({
158
+ route: value,
159
+ state,
160
+ logger,
161
+ sourceFile,
162
+ basePath: parentConfig.basePath,
163
+ inheritedTags: parentConfig.tags,
164
+ inheritedAuth: parentConfig.auth,
165
+ });
166
+ continue;
167
+ }
168
+ if (isExportedRouteContract(value)) {
169
+ processExportedRouteMap(value.routes, mergeConfigs(parentConfig, {
170
+ basePath: value.basePath || '',
171
+ tags: value.tags || [],
172
+ auth: value.auth,
173
+ }), state, logger, sourceFile);
174
+ continue;
175
+ }
176
+ processExportedRouteMap(value, parentConfig, state, logger, sourceFile);
177
+ }
178
+ }
179
+ function isExportedRouteConfig(value) {
180
+ return (typeof value === 'object' &&
181
+ value !== null &&
182
+ 'method' in value &&
183
+ 'route' in value &&
184
+ 'func' in value);
185
+ }
186
+ function isExportedRouteContract(value) {
187
+ return (typeof value === 'object' &&
188
+ value !== null &&
189
+ 'routes' in value &&
190
+ !('method' in value));
191
+ }
192
+ function serializeHTTPRoutesContract(node, checker, state) {
193
+ if (isRouteContract(node)) {
194
+ const routesProp = getPropertyAssignment(node, 'routes');
195
+ if (!routesProp || !ts.isObjectLiteralExpression(routesProp.initializer)) {
196
+ return null;
197
+ }
198
+ return {
199
+ ...extractGroupConfig(node),
200
+ routes: serializeHTTPRouteMap(routesProp.initializer, checker, state),
201
+ };
202
+ }
203
+ return {
204
+ routes: serializeHTTPRouteMap(node, checker, state),
205
+ };
206
+ }
207
+ function serializeHTTPRouteMap(node, checker, state) {
208
+ const result = {};
209
+ for (const prop of node.properties) {
210
+ if (!ts.isPropertyAssignment(prop))
211
+ continue;
212
+ const key = prop.name.getText().replace(/^['"]|['"]$/g, '');
213
+ const value = prop.initializer;
214
+ if (ts.isObjectLiteralExpression(value)) {
215
+ if (isRouteConfig(value)) {
216
+ const route = serializeHTTPRouteConfig(value, checker, state);
217
+ if (route) {
218
+ result[key] = route;
219
+ }
220
+ continue;
221
+ }
222
+ if (isRouteContract(value)) {
223
+ const routeContract = serializeHTTPRoutesContract(value, checker, state);
224
+ if (routeContract) {
225
+ result[key] = routeContract;
226
+ }
227
+ continue;
228
+ }
229
+ result[key] = serializeHTTPRouteMap(value, checker, state);
230
+ continue;
231
+ }
232
+ if (ts.isIdentifier(value)) {
233
+ const resolved = resolveIdentifier(value, checker, ['defineHTTPRoutes']);
234
+ if (resolved && ts.isObjectLiteralExpression(resolved)) {
235
+ if (isRouteContract(resolved)) {
236
+ const routeContract = serializeHTTPRoutesContract(resolved, checker, state);
237
+ if (routeContract) {
238
+ result[key] = routeContract;
239
+ }
240
+ }
241
+ else {
242
+ result[key] = serializeHTTPRouteMap(resolved, checker, state);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+ function serializeHTTPRouteConfig(obj, checker, state) {
250
+ const method = getPropertyValue(obj, 'method');
251
+ const route = getPropertyValue(obj, 'route');
252
+ const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
253
+ if (!method || !route || !funcInitializer) {
254
+ return null;
255
+ }
256
+ let pikkuFuncId = extractFunctionName(funcInitializer, checker, state.rootDir).pikkuFuncId;
257
+ let packageName;
258
+ if (ts.isCallExpression(funcInitializer) &&
259
+ ts.isIdentifier(funcInitializer.expression) &&
260
+ funcInitializer.expression.text === 'ref') {
261
+ const [firstArg] = funcInitializer.arguments;
262
+ if (firstArg && ts.isStringLiteral(firstArg)) {
263
+ pikkuFuncId = firstArg.text;
264
+ const addonNamespace = pikkuFuncId.includes(':')
265
+ ? pikkuFuncId.split(':')[0]
266
+ : null;
267
+ packageName = addonNamespace
268
+ ? state.rpc.wireAddonDeclarations.get(addonNamespace)?.package
269
+ : undefined;
270
+ }
271
+ }
272
+ else if (ts.isIdentifier(funcInitializer)) {
273
+ packageName =
274
+ resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations) || undefined;
275
+ }
276
+ return {
277
+ auth: getPropertyValue(obj, 'auth'),
278
+ contentType: getPropertyValue(obj, 'contentType'),
279
+ headers: getPropertyValue(obj, 'headers') ||
280
+ undefined,
281
+ method,
282
+ route,
283
+ sse: getPropertyValue(obj, 'sse'),
284
+ tags: getPropertyValue(obj, 'tags') || undefined,
285
+ timeout: getPropertyValue(obj, 'timeout'),
286
+ func: {
287
+ pikkuFuncId,
288
+ ...(packageName && { packageName }),
289
+ },
290
+ };
291
+ }
149
292
  function processRoute(obj, groupConfig, state, checker, logger, sourceFile) {
150
293
  registerHTTPRoute({
151
294
  obj,
@@ -57,7 +57,9 @@ export declare enum ErrorCode {
57
57
  SERVICES_NOT_DESTRUCTURED = "PKU410",
58
58
  WIRES_NOT_DESTRUCTURED = "PKU411",
59
59
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
60
- PII_IN_OUTPUT = "PKU910"
60
+ PII_IN_OUTPUT = "PKU910",
61
+ ADDON_WIRING_NOT_ALLOWED = "PKU920",
62
+ ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921"
61
63
  }
62
64
  /**
63
65
  * Severity of a tracked, coded diagnostic. `critical` always blocks the build;
@@ -72,4 +72,7 @@ export var ErrorCode;
72
72
  ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
73
73
  // Data classification errors
74
74
  ErrorCode["PII_IN_OUTPUT"] = "PKU910";
75
+ // Addon authoring errors
76
+ ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
77
+ ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
75
78
  })(ErrorCode || (ErrorCode = {}));
package/dist/inspector.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as ts from 'typescript';
2
2
  import { performance } from 'perf_hooks';
3
+ import { resolve } from 'path';
3
4
  import { visitSetup, visitRoutes } from './visit.js';
4
5
  import { TypesMap } from './types-map.js';
5
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
@@ -185,10 +186,19 @@ export function getInitialInspectorState(rootDir) {
185
186
  openAPISpec: null,
186
187
  diagnostics: [],
187
188
  addonFunctions: {},
189
+ exportedContracts: {
190
+ http: {},
191
+ cli: {},
192
+ channel: {},
193
+ addonHttp: {},
194
+ addonCli: {},
195
+ addonChannel: {},
196
+ },
188
197
  program: null,
189
198
  };
190
199
  }
191
200
  export const inspect = async (logger, routeFiles, options = {}) => {
201
+ const normalizedRouteFiles = routeFiles.map((file) => resolve(file));
192
202
  const compilerOptions = {
193
203
  target: ts.ScriptTarget.ESNext,
194
204
  module: ts.ModuleKind.Node16,
@@ -200,14 +210,14 @@ export const inspect = async (logger, routeFiles, options = {}) => {
200
210
  checkJs: false,
201
211
  };
202
212
  const startProgram = performance.now();
203
- const program = ts.createProgram(routeFiles, compilerOptions, undefined, // host
213
+ const program = ts.createProgram(normalizedRouteFiles, compilerOptions, undefined, // host
204
214
  options.oldProgram);
205
- logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
215
+ logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
206
216
  const startChecker = performance.now();
207
217
  const checker = program.getTypeChecker();
208
218
  logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
209
219
  // Use provided rootDir or infer from source files
210
- const rootDir = options.rootDir || findCommonAncestor(routeFiles);
220
+ const rootDir = options.rootDir || findCommonAncestor(normalizedRouteFiles);
211
221
  const startSourceFiles = performance.now();
212
222
  // node_modules under rootDir (e.g. a locally-installed addon) is a
213
223
  // dependency, not project source — scanning it double-counts the addon's
@@ -221,7 +231,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
221
231
  // First sweep: add all functions
222
232
  const startSetup = performance.now();
223
233
  for (const sourceFile of sourceFiles) {
224
- ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, options));
234
+ const sourceOptions = { ...options, sourceFile };
235
+ ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, sourceOptions));
225
236
  }
226
237
  logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
227
238
  // Load addon function metadata so wirings can reference addon functions
@@ -230,7 +241,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
230
241
  // Second sweep: add all transports
231
242
  const startRoutes = performance.now();
232
243
  for (const sourceFile of sourceFiles) {
233
- ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, options));
244
+ const sourceOptions = { ...options, sourceFile };
245
+ ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
234
246
  }
235
247
  logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
236
248
  resolveLatestVersions(state, logger);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as ts from 'typescript';
2
- import type { ChannelsMeta } from '@pikku/core/channel';
2
+ import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
3
3
  import type { GatewaysMeta } from '@pikku/core/gateway';
4
4
  import type { HTTPWiringsMeta } from '@pikku/core/http';
5
5
  import type { ScheduledTasksMeta } from '@pikku/core/scheduler';
@@ -9,6 +9,7 @@ import type { WorkflowsMeta } from '@pikku/core/workflow';
9
9
  import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
10
10
  import type { AIAgentMeta } from '@pikku/core/ai-agent';
11
11
  import type { CLIMeta } from '@pikku/core/cli';
12
+ import type { CLICommandMeta } from '@pikku/core/cli';
12
13
  import type { NodesMeta } from '@pikku/core/node';
13
14
  import type { SecretDefinitions } from '@pikku/core/secret';
14
15
  import type { CredentialDefinitions } from '@pikku/core/credential';
@@ -80,6 +81,45 @@ export interface InspectorChannelState {
80
81
  meta: ChannelsMeta;
81
82
  files: Set<string>;
82
83
  }
84
+ export interface ExportedHTTPRouteFunctionMeta {
85
+ pikkuFuncId: string;
86
+ packageName?: string;
87
+ }
88
+ export interface ExportedHTTPRouteConfigMeta {
89
+ method: string;
90
+ route: string;
91
+ func: ExportedHTTPRouteFunctionMeta;
92
+ auth?: boolean;
93
+ tags?: string[];
94
+ sse?: boolean;
95
+ contentType?: string;
96
+ timeout?: number;
97
+ headers?: Record<string, string>;
98
+ }
99
+ export interface ExportedHTTPRoutesGroupMeta {
100
+ basePath?: string;
101
+ tags?: string[];
102
+ auth?: boolean;
103
+ routes: ExportedHTTPRouteMapMeta;
104
+ }
105
+ export type ExportedHTTPRouteEntryMeta = ExportedHTTPRouteConfigMeta | ExportedHTTPRoutesGroupMeta | ExportedHTTPRouteMapMeta;
106
+ export interface ExportedHTTPRouteMapMeta {
107
+ [key: string]: ExportedHTTPRouteEntryMeta;
108
+ }
109
+ export type ExportedHTTPContractsMeta = Record<string, ExportedHTTPRoutesGroupMeta>;
110
+ export interface ExportedChannelRouteMeta extends ChannelMessageMeta {
111
+ auth?: boolean;
112
+ }
113
+ export type ExportedChannelContractsMeta = Record<string, Record<string, ExportedChannelRouteMeta>>;
114
+ export type ExportedCLIContractsMeta = Record<string, Record<string, CLICommandMeta>>;
115
+ export interface InspectorExportedContractsState {
116
+ http: ExportedHTTPContractsMeta;
117
+ cli: ExportedCLIContractsMeta;
118
+ channel: ExportedChannelContractsMeta;
119
+ addonHttp: Record<string, ExportedHTTPContractsMeta>;
120
+ addonCli: Record<string, ExportedCLIContractsMeta>;
121
+ addonChannel: Record<string, ExportedChannelContractsMeta>;
122
+ }
83
123
  export interface InspectorMiddlewareDefinition {
84
124
  services: FunctionServicesMeta;
85
125
  wires?: FunctionWiresMeta;
@@ -167,6 +207,7 @@ export type InspectorOptions = Partial<{
167
207
  setupOnly: boolean;
168
208
  rootDir: string;
169
209
  isAddon: boolean;
210
+ sourceFile: ts.SourceFile;
170
211
  types: Partial<{
171
212
  configFileType: string;
172
213
  userSessionType: string;
@@ -390,6 +431,11 @@ export interface InspectorState {
390
431
  * codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
391
432
  * More than one `pikkuBetterAuth` is a critical error. */
392
433
  definition: AuthDefinition | null;
434
+ /** True when a user (non-generated) file already registers
435
+ * `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
436
+ * own default-map stateless middleware, which would otherwise pre-empt the
437
+ * user's custom mapSession (pikkujs/pikku#754). */
438
+ userStatelessSession?: boolean;
393
439
  };
394
440
  secrets: {
395
441
  definitions: SecretDefinitions;
@@ -444,5 +490,6 @@ export interface InspectorState {
444
490
  openAPISpec: Record<string, any> | null;
445
491
  diagnostics: InspectorDiagnostic[];
446
492
  addonFunctions: Record<string, FunctionsMeta>;
493
+ exportedContracts: InspectorExportedContractsState;
447
494
  program: ts.Program | null;
448
495
  }
@@ -0,0 +1,2 @@
1
+ import * as ts from 'typescript';
2
+ export declare const getExportedVariableName: (node: ts.Node, sourceFile: ts.SourceFile | undefined) => string | null;
@@ -0,0 +1,34 @@
1
+ import * as ts from 'typescript';
2
+ const isExportedVariableStatement = (statement) => ts.isVariableStatement(statement) &&
3
+ (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
4
+ false);
5
+ export const getExportedVariableName = (node, sourceFile) => {
6
+ if (!sourceFile) {
7
+ return null;
8
+ }
9
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
10
+ for (const statement of sourceFile.statements) {
11
+ if (!isExportedVariableStatement(statement))
12
+ continue;
13
+ for (const declaration of statement.declarationList.declarations) {
14
+ if (declaration === node) {
15
+ return node.name.text;
16
+ }
17
+ }
18
+ }
19
+ }
20
+ if (!ts.isCallExpression(node)) {
21
+ return null;
22
+ }
23
+ for (const statement of sourceFile.statements) {
24
+ if (!isExportedVariableStatement(statement))
25
+ continue;
26
+ for (const declaration of statement.declarationList.declarations) {
27
+ if (ts.isIdentifier(declaration.name) &&
28
+ declaration.initializer === node) {
29
+ return declaration.name.text;
30
+ }
31
+ }
32
+ }
33
+ return null;
34
+ };
@@ -1,6 +1,74 @@
1
1
  import { readFile, readdir } from 'fs/promises';
2
2
  import { createRequire } from 'module';
3
3
  import { join, dirname } from 'path';
4
+ const isHTTPRouteConfig = (value) => typeof value === 'object' &&
5
+ value !== null &&
6
+ 'method' in value &&
7
+ 'func' in value &&
8
+ 'route' in value;
9
+ const isHTTPRouteGroup = (value) => typeof value === 'object' &&
10
+ value !== null &&
11
+ 'routes' in value &&
12
+ !('method' in value);
13
+ const applyPackageToHTTPRouteMap = (routes, packageName, namespace) => {
14
+ for (const value of Object.values(routes)) {
15
+ if (!value || typeof value !== 'object')
16
+ continue;
17
+ if (isHTTPRouteConfig(value)) {
18
+ if (!value.func.packageName) {
19
+ value.func.packageName = packageName;
20
+ }
21
+ if (namespace && !value.func.pikkuFuncId.includes(':')) {
22
+ value.func.pikkuFuncId = `${namespace}:${value.func.pikkuFuncId}`;
23
+ }
24
+ continue;
25
+ }
26
+ if (isHTTPRouteGroup(value)) {
27
+ applyPackageToHTTPRouteMap(value.routes, packageName, namespace);
28
+ continue;
29
+ }
30
+ applyPackageToHTTPRouteMap(value, packageName, namespace);
31
+ }
32
+ };
33
+ const applyPackageToHTTPContracts = (contracts, packageName, namespace) => {
34
+ for (const contract of Object.values(contracts)) {
35
+ applyPackageToHTTPRouteMap(contract.routes, packageName, namespace);
36
+ }
37
+ };
38
+ const applyPackageToCLICommands = (commands, packageName, namespace) => {
39
+ for (const command of Object.values(commands)) {
40
+ if (command && typeof command === 'object') {
41
+ if (!command.packageName && command.pikkuFuncId) {
42
+ command.packageName = packageName;
43
+ }
44
+ if (namespace &&
45
+ typeof command.pikkuFuncId === 'string' &&
46
+ !command.pikkuFuncId.includes(':')) {
47
+ command.pikkuFuncId = `${namespace}:${command.pikkuFuncId}`;
48
+ }
49
+ if (command.subcommands) {
50
+ applyPackageToCLICommands(command.subcommands, packageName, namespace);
51
+ }
52
+ }
53
+ }
54
+ };
55
+ const applyPackageToCLIContracts = (contracts, packageName, namespace) => {
56
+ for (const commands of Object.values(contracts)) {
57
+ applyPackageToCLICommands(commands, packageName, namespace);
58
+ }
59
+ };
60
+ const applyPackageToChannelContracts = (contracts, packageName, namespace) => {
61
+ for (const routes of Object.values(contracts)) {
62
+ for (const route of Object.values(routes)) {
63
+ if (!route.packageName) {
64
+ route.packageName = packageName;
65
+ }
66
+ if (!route.pikkuFuncId.includes(':')) {
67
+ route.pikkuFuncId = `${namespace}:${route.pikkuFuncId}`;
68
+ }
69
+ }
70
+ }
71
+ };
4
72
  /**
5
73
  * After the setup sweep discovers wireAddon() declarations, load each addon
6
74
  * package's function metadata so that wiring handlers (channels, HTTP routes,
@@ -81,6 +149,36 @@ export async function loadAddonFunctionsMeta(logger, state) {
81
149
  catch {
82
150
  // No services gen — addon may not have requiredParentServices
83
151
  }
152
+ try {
153
+ const httpContractsPath = require.resolve(`${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`);
154
+ const httpContractsRaw = await readFile(httpContractsPath, 'utf-8');
155
+ const httpContracts = JSON.parse(httpContractsRaw);
156
+ applyPackageToHTTPContracts(httpContracts, decl.package, namespace);
157
+ state.exportedContracts.addonHttp[namespace] = httpContracts;
158
+ }
159
+ catch {
160
+ // No addon HTTP contracts metadata
161
+ }
162
+ try {
163
+ const cliContractsPath = require.resolve(`${decl.package}/.pikku/cli/pikku-cli-contracts-meta.gen.json`);
164
+ const cliContractsRaw = await readFile(cliContractsPath, 'utf-8');
165
+ const cliContracts = JSON.parse(cliContractsRaw);
166
+ applyPackageToCLIContracts(cliContracts, decl.package, namespace);
167
+ state.exportedContracts.addonCli[namespace] = cliContracts;
168
+ }
169
+ catch {
170
+ // No addon CLI contracts metadata
171
+ }
172
+ try {
173
+ const channelContractsPath = require.resolve(`${decl.package}/.pikku/channel/pikku-channel-contracts-meta.gen.json`);
174
+ const channelContractsRaw = await readFile(channelContractsPath, 'utf-8');
175
+ const channelContracts = JSON.parse(channelContractsRaw);
176
+ applyPackageToChannelContracts(channelContracts, decl.package, namespace);
177
+ state.exportedContracts.addonChannel[namespace] = channelContracts;
178
+ }
179
+ catch {
180
+ // No addon channel contracts metadata
181
+ }
84
182
  }
85
183
  catch (error) {
86
184
  logger.warn(`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`);
@@ -245,9 +245,22 @@ export function aggregateRequiredServices(state) {
245
245
  if (Object.keys(state.channels.meta).length > 0) {
246
246
  requiredServices.add('eventHub');
247
247
  }
248
- // 7. Services that addons need from the parent project
249
- for (const service of state.addonRequiredParentServices ?? []) {
250
- requiredServices.add(service);
248
+ // 7. Services that consumed addons need from the parent project.
249
+ // These are required ONLY by units that actually deploy an addon function;
250
+ // a unit that merely calls the addon over RPC (or never touches it) must not
251
+ // carry them, or every per-unit bundle would over-include the addon's
252
+ // parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
253
+ // defeat per-unit tree-shaking.
254
+ const addonFuncIds = new Set();
255
+ for (const fns of Object.values(state.addonFunctions ?? {})) {
256
+ for (const id of Object.keys(fns))
257
+ addonFuncIds.add(id);
258
+ }
259
+ const unitDeploysAddonFn = [...usedFunctions].some((fn) => addonFuncIds.has(fn));
260
+ if (unitDeploysAddonFn) {
261
+ for (const service of state.addonRequiredParentServices ?? []) {
262
+ requiredServices.add(service);
263
+ }
251
264
  }
252
265
  }
253
266
  export function validateSecretOverrides(logger, state) {
@@ -61,8 +61,10 @@ export const resolveAddonName = (identifier, checker, wireAddonDeclarations) =>
61
61
  // Bare package import path
62
62
  if (candidatePackage && !candidatePackage.startsWith('.')) {
63
63
  for (const addonDecl of wireAddonDeclarations.values()) {
64
- if (addonDecl.package === candidatePackage)
64
+ if (addonDecl.package === candidatePackage ||
65
+ candidatePackage.startsWith(`${addonDecl.package}/`)) {
65
66
  return addonDecl.package;
67
+ }
66
68
  }
67
69
  }
68
70
  // Fall back to package.json lookup based on the declaration's source file.