@pikku/inspector 0.12.21 → 0.12.23

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 (61) hide show
  1. package/CHANGELOG.md +30 -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-channel.js +47 -6
  5. package/dist/add/add-cli.js +17 -0
  6. package/dist/add/add-functions.js +16 -8
  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/add/add-workflow.js +16 -2
  12. package/dist/error-codes.d.ts +15 -1
  13. package/dist/error-codes.js +3 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/inspector.js +22 -6
  16. package/dist/types.d.ts +53 -2
  17. package/dist/utils/extract-node-value.js +19 -2
  18. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  19. package/dist/utils/get-exported-variable-name.js +34 -0
  20. package/dist/utils/load-addon-functions-meta.js +98 -0
  21. package/dist/utils/resolve-addon-package.js +3 -1
  22. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  23. package/dist/utils/resolve-ref-contract.js +46 -0
  24. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  25. package/dist/utils/serialize-inspector-state.js +9 -0
  26. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +15 -0
  27. package/dist/visit.js +24 -19
  28. package/package.json +2 -2
  29. package/src/add/add-addon-bans.ts +84 -0
  30. package/src/add/add-auth.test.ts +3 -0
  31. package/src/add/add-channel.ts +66 -7
  32. package/src/add/add-cli-renderers.test.ts +1 -0
  33. package/src/add/add-cli.ts +30 -0
  34. package/src/add/add-functions.test.ts +13 -0
  35. package/src/add/add-functions.ts +14 -10
  36. package/src/add/add-http-route.ts +75 -1
  37. package/src/add/add-http-routes.ts +283 -41
  38. package/src/add/add-workflow-fanout.test.ts +106 -0
  39. package/src/add/add-workflow.test.ts +3 -0
  40. package/src/add/add-workflow.ts +16 -2
  41. package/src/add/addon-bans.test.ts +121 -0
  42. package/src/add/addon-contracts.test.ts +221 -0
  43. package/src/add/pii-check.test.ts +4 -0
  44. package/src/add/wire-name-literal.test.ts +3 -0
  45. package/src/error-codes.ts +18 -0
  46. package/src/index.ts +1 -0
  47. package/src/inspector.ts +25 -6
  48. package/src/types.ts +75 -2
  49. package/src/utils/extract-node-value.test.ts +49 -1
  50. package/src/utils/extract-node-value.ts +19 -2
  51. package/src/utils/filter-inspector-state.test.ts +1 -0
  52. package/src/utils/filter-utils.test.ts +1 -0
  53. package/src/utils/get-exported-variable-name.ts +48 -0
  54. package/src/utils/load-addon-functions-meta.ts +164 -0
  55. package/src/utils/resolve-addon-package.ts +6 -1
  56. package/src/utils/resolve-ref-contract.ts +71 -0
  57. package/src/utils/resolve-versions.test.ts +1 -0
  58. package/src/utils/serialize-inspector-state.ts +10 -0
  59. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +16 -0
  60. package/src/visit.ts +26 -19
  61. package/tsconfig.tsbuildinfo +1 -1
@@ -5,6 +5,20 @@ import { ErrorCode } from '../error-codes.js';
5
5
  import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
6
  import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
7
7
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
8
+ import { getSourceText } from '../utils/workflow/dsl/patterns.js';
9
+ /**
10
+ * Extract a workflow step's display name without letting a non-static name
11
+ * (e.g. a function call) abort the scan. The step name is cosmetic, so a
12
+ * resolution failure must never prevent the RPC from being registered.
13
+ */
14
+ function extractStepName(node, checker) {
15
+ try {
16
+ return extractStringLiteral(node, checker);
17
+ }
18
+ catch {
19
+ return getSourceText(node);
20
+ }
21
+ }
8
22
  /**
9
23
  * Recursively check if any step has inline type (non-serializable)
10
24
  */
@@ -89,7 +103,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
89
103
  const stepNameArg = args[0];
90
104
  const secondArg = args[1];
91
105
  const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
92
- const stepName = extractStringLiteral(stepNameArg, checker);
106
+ const stepName = extractStepName(stepNameArg, checker);
93
107
  const description = extractDescription(optionsArg, checker) ?? undefined;
94
108
  // Determine form by checking 2nd argument type
95
109
  if (isStringLike(secondArg, checker)) {
@@ -115,7 +129,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
115
129
  // workflow.sleep(stepName, duration)
116
130
  const stepNameArg = args[0];
117
131
  const durationArg = args[1];
118
- const stepName = extractStringLiteral(stepNameArg, checker);
132
+ const stepName = extractStepName(stepNameArg, checker);
119
133
  const duration = extractDuration(durationArg, checker);
120
134
  steps.push({
121
135
  type: 'sleep',
@@ -57,5 +57,19 @@ 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"
63
+ }
64
+ /**
65
+ * Severity of a tracked, coded diagnostic. `critical` always blocks the build;
66
+ * `error`/`warn` only block when the CLI is told to via `--fail-on-error` /
67
+ * `--fail-on-warn` (default: critical only). All severities are still printed.
68
+ */
69
+ export type DiagnosticSeverity = 'warn' | 'error' | 'critical';
70
+ /** A coded diagnostic emitted via `logger.diagnostic(...)`. */
71
+ export interface CodedDiagnostic {
72
+ severity: DiagnosticSeverity;
73
+ code: ErrorCode;
74
+ message: string;
61
75
  }
@@ -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/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { TypesMap } from './types-map.js';
3
3
  export type * from './types.js';
4
4
  export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
5
5
  export { ErrorCode } from './error-codes.js';
6
+ export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
6
7
  export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
7
8
  export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
8
9
  export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
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,24 +210,29 @@ 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();
222
+ // node_modules under rootDir (e.g. a locally-installed addon) is a
223
+ // dependency, not project source — scanning it double-counts the addon's
224
+ // own application types (CoreConfig/Services/SingletonServices).
212
225
  const sourceFiles = program
213
226
  .getSourceFiles()
214
- .filter((sf) => sf.fileName.startsWith(rootDir));
227
+ .filter((sf) => sf.fileName.startsWith(rootDir) &&
228
+ !sf.fileName.includes('/node_modules/'));
215
229
  logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
216
230
  const state = getInitialInspectorState(rootDir);
217
231
  // First sweep: add all functions
218
232
  const startSetup = performance.now();
219
233
  for (const sourceFile of sourceFiles) {
220
- 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));
221
236
  }
222
237
  logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
223
238
  // Load addon function metadata so wirings can reference addon functions
@@ -226,7 +241,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
226
241
  // Second sweep: add all transports
227
242
  const startRoutes = performance.now();
228
243
  for (const sourceFile of sourceFiles) {
229
- 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));
230
246
  }
231
247
  logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
232
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';
@@ -16,7 +17,7 @@ import type { VariableDefinitions } from '@pikku/core/variable';
16
17
  import type { TypesMap } from './types-map.js';
17
18
  import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
18
19
  import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
19
- import type { ErrorCode } from './error-codes.js';
20
+ import type { ErrorCode, CodedDiagnostic } from './error-codes.js';
20
21
  import type { VersionManifest, VersionValidateError } from './utils/contract-hashes.js';
21
22
  import type { SerializedWorkflowGraphs } from './utils/workflow/graph/workflow-graph.types.js';
22
23
  export type PathToNameAndType = Map<string, {
@@ -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;
@@ -192,6 +233,15 @@ export interface InspectorLogger {
192
233
  error: (message: string) => void;
193
234
  warn: (message: string) => void;
194
235
  debug: (message: string) => void;
236
+ /**
237
+ * Emit a tracked, coded diagnostic. It is recorded and printed; `error`/`warn`
238
+ * only block the build when the CLI is run with `--fail-on-error` /
239
+ * `--fail-on-warn` (default: critical only). Use this for issues worth
240
+ * surfacing (e.g. data-classification leaks) that should not stop the dev
241
+ * server from starting.
242
+ */
243
+ diagnostic: (diagnostic: CodedDiagnostic) => void;
244
+ /** Sugar for `diagnostic({ severity: 'critical', code, message })`. */
195
245
  critical: (code: ErrorCode, message: string) => void;
196
246
  hasCriticalErrors: () => boolean;
197
247
  }
@@ -435,5 +485,6 @@ export interface InspectorState {
435
485
  openAPISpec: Record<string, any> | null;
436
486
  diagnostics: InspectorDiagnostic[];
437
487
  addonFunctions: Record<string, FunctionsMeta>;
488
+ exportedContracts: InspectorExportedContractsState;
438
489
  program: ts.Program | null;
439
490
  }
@@ -26,8 +26,8 @@ export function extractStringLiteral(node, checker) {
26
26
  }
27
27
  if (ts.isBinaryExpression(node) &&
28
28
  node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
29
- return (extractStringLiteral(node.left, checker) +
30
- extractStringLiteral(node.right, checker));
29
+ return (extractConcatOperand(node.left, checker) +
30
+ extractConcatOperand(node.right, checker));
31
31
  }
32
32
  // Try to evaluate constant identifiers
33
33
  if (ts.isIdentifier(node)) {
@@ -42,6 +42,23 @@ export function extractStringLiteral(node, checker) {
42
42
  }
43
43
  throw new Error('Unable to extract string literal from node');
44
44
  }
45
+ /**
46
+ * Resolve one operand of a `+` string concatenation.
47
+ *
48
+ * An operand that can't be statically resolved (e.g. `a ?? b`) becomes a
49
+ * `${...}` placeholder rather than throwing — mirroring the TemplateExpression
50
+ * branch above, so `'x ' + expr` and `` `x ${expr}` `` produce the same string.
51
+ * This keeps an unresolvable display name from aborting the whole extraction.
52
+ */
53
+ function extractConcatOperand(node, checker) {
54
+ try {
55
+ return extractStringLiteral(node, checker);
56
+ }
57
+ catch {
58
+ const inner = ts.isParenthesizedExpression(node) ? node.expression : node;
59
+ return '${' + inner.getText() + '}';
60
+ }
61
+ }
45
62
  /**
46
63
  * Check if node is string-like (string literal or template expression)
47
64
  */
@@ -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}`);
@@ -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.
@@ -0,0 +1,21 @@
1
+ import * as ts from 'typescript';
2
+ export interface RefContractResolution<T> {
3
+ contract: T;
4
+ /**
5
+ * Optional basePath override supplied by the consumer via the second
6
+ * argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
7
+ * the addon contract's own basePath is preserved.
8
+ */
9
+ basePath?: string;
10
+ }
11
+ /**
12
+ * Resolve a refHTTP / refChannel / refCLI call expression against the addon
13
+ * contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
14
+ *
15
+ * The first string argument has the form 'namespace:contractName', mirroring
16
+ * how ref('namespace:fn') references an addon function. Detection is purely
17
+ * syntactic — no import resolution is required because the namespace and
18
+ * contract name are carried in the string literal. An optional second object
19
+ * argument may override mount details such as basePath.
20
+ */
21
+ export declare const resolveRefContract: <T>(node: ts.Node, helperName: "refHTTP" | "refChannel" | "refCLI", addonContracts: Record<string, Record<string, T>>) => RefContractResolution<T> | null;
@@ -0,0 +1,46 @@
1
+ import * as ts from 'typescript';
2
+ const getStringProperty = (obj, name) => {
3
+ for (const prop of obj.properties) {
4
+ if (ts.isPropertyAssignment(prop) &&
5
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
6
+ prop.name.text === name &&
7
+ ts.isStringLiteral(prop.initializer)) {
8
+ return prop.initializer.text;
9
+ }
10
+ }
11
+ return undefined;
12
+ };
13
+ /**
14
+ * Resolve a refHTTP / refChannel / refCLI call expression against the addon
15
+ * contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
16
+ *
17
+ * The first string argument has the form 'namespace:contractName', mirroring
18
+ * how ref('namespace:fn') references an addon function. Detection is purely
19
+ * syntactic — no import resolution is required because the namespace and
20
+ * contract name are carried in the string literal. An optional second object
21
+ * argument may override mount details such as basePath.
22
+ */
23
+ export const resolveRefContract = (node, helperName, addonContracts) => {
24
+ if (!ts.isCallExpression(node))
25
+ return null;
26
+ if (!ts.isIdentifier(node.expression) ||
27
+ node.expression.text !== helperName) {
28
+ return null;
29
+ }
30
+ const [arg, optionsArg] = node.arguments;
31
+ if (!arg || !ts.isStringLiteral(arg))
32
+ return null;
33
+ const separator = arg.text.indexOf(':');
34
+ if (separator === -1)
35
+ return null;
36
+ const namespace = arg.text.slice(0, separator);
37
+ const contractName = arg.text.slice(separator + 1);
38
+ const contract = addonContracts[namespace]?.[contractName];
39
+ if (contract === undefined)
40
+ return null;
41
+ let basePath;
42
+ if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
43
+ basePath = getStringProperty(optionsArg, 'basePath');
44
+ }
45
+ return { contract, basePath };
46
+ };
@@ -267,6 +267,7 @@ export interface SerializableInspectorState {
267
267
  openAPISpec: Record<string, any> | null;
268
268
  diagnostics: InspectorDiagnostic[];
269
269
  addonFunctions: InspectorState['addonFunctions'];
270
+ exportedContracts: InspectorState['exportedContracts'];
270
271
  }
271
272
  /**
272
273
  * Serializes InspectorState to a JSON-compatible format
@@ -150,6 +150,7 @@ export function serializeInspectorState(state) {
150
150
  openAPISpec: state.openAPISpec,
151
151
  diagnostics: state.diagnostics,
152
152
  addonFunctions: state.addonFunctions,
153
+ exportedContracts: state.exportedContracts,
153
154
  };
154
155
  }
155
156
  /**
@@ -314,6 +315,14 @@ export function deserializeInspectorState(data) {
314
315
  openAPISpec: data.openAPISpec || null,
315
316
  diagnostics: data.diagnostics || [],
316
317
  addonFunctions: data.addonFunctions || {},
318
+ exportedContracts: data.exportedContracts || {
319
+ http: {},
320
+ cli: {},
321
+ channel: {},
322
+ addonHttp: {},
323
+ addonCli: {},
324
+ addonChannel: {},
325
+ },
317
326
  program: null,
318
327
  };
319
328
  }
@@ -255,6 +255,21 @@ function extractVariableDeclaration(statement, context) {
255
255
  return step;
256
256
  }
257
257
  }
258
+ // Promise.all fanout/group captured into a variable
259
+ // (const results = await Promise.all(array.map(...)))
260
+ if (isParallelFanout(call) || isParallelGroup(call)) {
261
+ const step = isParallelFanout(call)
262
+ ? extractParallelFanout(call, context)
263
+ : extractParallelGroup(call, context);
264
+ if (step) {
265
+ const type = context.checker.getTypeAtLocation(decl);
266
+ context.outputVars.set(varName, { type, node: decl });
267
+ if (isArrayType(type, context.checker)) {
268
+ context.arrayVars.add(varName);
269
+ }
270
+ return step;
271
+ }
272
+ }
258
273
  }
259
274
  // Check for array.filter(...)
260
275
  if (ts.isCallExpression(init)) {
package/dist/visit.js CHANGED
@@ -3,6 +3,7 @@ import { addFileWithFactory } from './add/add-file-with-factory.js';
3
3
  import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
4
4
  import { addHTTPRoute } from './add/add-http-route.js';
5
5
  import { addHTTPRoutes } from './add/add-http-routes.js';
6
+ import { checkAddonBans } from './add/add-addon-bans.js';
6
7
  import { addSchedule } from './add/add-schedule.js';
7
8
  import { addTrigger } from './add/add-trigger.js';
8
9
  import { addQueueWorker } from './add/add-queue-worker.js';
@@ -41,23 +42,27 @@ export const visitSetup = (logger, checker, node, state, options) => {
41
42
  ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
42
43
  };
43
44
  export const visitRoutes = (logger, checker, node, state, options) => {
44
- addFunctions(logger, node, checker, state, options);
45
- addAuth(logger, node, checker, state, options);
46
- addSecret(logger, node, checker, state, options);
47
- addCredential(logger, node, checker, state, options);
48
- addVariable(logger, node, checker, state, options);
49
- addHTTPRoute(logger, node, checker, state, options);
50
- addHTTPRoutes(logger, node, checker, state, options);
51
- addSchedule(logger, node, checker, state, options);
52
- addTrigger(logger, node, checker, state, options);
53
- addQueueWorker(logger, node, checker, state, options);
54
- addChannel(logger, node, checker, state, options);
55
- addGateway(logger, node, checker, state, options);
56
- addCLI(logger, node, checker, state, options);
57
- addCLIRenderers(logger, node, checker, state, options);
58
- addMCPResource(logger, node, checker, state, options);
59
- addMCPPrompt(logger, node, checker, state, options);
60
- addWorkflowGraph(logger, node, checker, state, options);
61
- addAIAgent(logger, node, checker, state, options);
62
- ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, options));
45
+ const nextOptions = ts.isSourceFile(node)
46
+ ? { ...options, sourceFile: node }
47
+ : options;
48
+ checkAddonBans(logger, node, checker, state, nextOptions);
49
+ addFunctions(logger, node, checker, state, nextOptions);
50
+ addAuth(logger, node, checker, state, nextOptions);
51
+ addSecret(logger, node, checker, state, nextOptions);
52
+ addCredential(logger, node, checker, state, nextOptions);
53
+ addVariable(logger, node, checker, state, nextOptions);
54
+ addHTTPRoute(logger, node, checker, state, nextOptions);
55
+ addHTTPRoutes(logger, node, checker, state, nextOptions);
56
+ addSchedule(logger, node, checker, state, nextOptions);
57
+ addTrigger(logger, node, checker, state, nextOptions);
58
+ addQueueWorker(logger, node, checker, state, nextOptions);
59
+ addChannel(logger, node, checker, state, nextOptions);
60
+ addGateway(logger, node, checker, state, nextOptions);
61
+ addCLI(logger, node, checker, state, nextOptions);
62
+ addCLIRenderers(logger, node, checker, state, nextOptions);
63
+ addMCPResource(logger, node, checker, state, nextOptions);
64
+ addMCPPrompt(logger, node, checker, state, nextOptions);
65
+ addWorkflowGraph(logger, node, checker, state, nextOptions);
66
+ addAIAgent(logger, node, checker, state, nextOptions);
67
+ ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
63
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.21",
3
+ "version": "0.12.23",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.32",
38
+ "@pikku/core": "^0.12.35",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "^2.5.0",