@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
@@ -37,8 +37,8 @@ export function extractStringLiteral(
37
37
  node.operatorToken.kind === ts.SyntaxKind.PlusToken
38
38
  ) {
39
39
  return (
40
- extractStringLiteral(node.left, checker) +
41
- extractStringLiteral(node.right, checker)
40
+ extractConcatOperand(node.left, checker) +
41
+ extractConcatOperand(node.right, checker)
42
42
  )
43
43
  }
44
44
 
@@ -59,6 +59,23 @@ export function extractStringLiteral(
59
59
  throw new Error('Unable to extract string literal from node')
60
60
  }
61
61
 
62
+ /**
63
+ * Resolve one operand of a `+` string concatenation.
64
+ *
65
+ * An operand that can't be statically resolved (e.g. `a ?? b`) becomes a
66
+ * `${...}` placeholder rather than throwing — mirroring the TemplateExpression
67
+ * branch above, so `'x ' + expr` and `` `x ${expr}` `` produce the same string.
68
+ * This keeps an unresolvable display name from aborting the whole extraction.
69
+ */
70
+ function extractConcatOperand(node: ts.Node, checker: ts.TypeChecker): string {
71
+ try {
72
+ return extractStringLiteral(node, checker)
73
+ } catch {
74
+ const inner = ts.isParenthesizedExpression(node) ? node.expression : node
75
+ return '${' + inner.getText() + '}'
76
+ }
77
+ }
78
+
62
79
  /**
63
80
  * Check if node is string-like (string literal or template expression)
64
81
  */
@@ -344,6 +344,7 @@ const mockLogger = {
344
344
  error: () => {},
345
345
  warn: () => {},
346
346
  debug: () => {},
347
+ diagnostic: () => {},
347
348
  critical: () => {},
348
349
  hasCriticalErrors: () => false,
349
350
  }
@@ -10,6 +10,7 @@ describe('matchesFilters', () => {
10
10
  error: () => {},
11
11
  warn: () => {},
12
12
  debug: () => {},
13
+ diagnostic: () => {},
13
14
  critical: () => {},
14
15
  hasCriticalErrors: () => false,
15
16
  }
@@ -0,0 +1,48 @@
1
+ import * as ts from 'typescript'
2
+
3
+ const isExportedVariableStatement = (
4
+ statement: ts.Statement
5
+ ): statement is ts.VariableStatement =>
6
+ ts.isVariableStatement(statement) &&
7
+ (statement.modifiers?.some(
8
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
9
+ ) ??
10
+ false)
11
+
12
+ export const getExportedVariableName = (
13
+ node: ts.Node,
14
+ sourceFile: ts.SourceFile | undefined
15
+ ): string | null => {
16
+ if (!sourceFile) {
17
+ return null
18
+ }
19
+
20
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
21
+ for (const statement of sourceFile.statements) {
22
+ if (!isExportedVariableStatement(statement)) continue
23
+ for (const declaration of statement.declarationList.declarations) {
24
+ if (declaration === node) {
25
+ return node.name.text
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ if (!ts.isCallExpression(node)) {
32
+ return null
33
+ }
34
+
35
+ for (const statement of sourceFile.statements) {
36
+ if (!isExportedVariableStatement(statement)) continue
37
+ for (const declaration of statement.declarationList.declarations) {
38
+ if (
39
+ ts.isIdentifier(declaration.name) &&
40
+ declaration.initializer === node
41
+ ) {
42
+ return declaration.name.text
43
+ }
44
+ }
45
+ }
46
+
47
+ return null
48
+ }
@@ -2,6 +2,121 @@ import { readFile, readdir } from 'fs/promises'
2
2
  import { createRequire } from 'module'
3
3
  import { join, dirname } from 'path'
4
4
  import type { InspectorState, InspectorLogger } from '../types.js'
5
+ import type {
6
+ ExportedChannelContractsMeta,
7
+ ExportedHTTPRouteConfigMeta,
8
+ ExportedHTTPRouteEntryMeta,
9
+ ExportedHTTPRoutesGroupMeta,
10
+ ExportedCLIContractsMeta,
11
+ ExportedHTTPContractsMeta,
12
+ ExportedHTTPRouteMapMeta,
13
+ } from '../types.js'
14
+
15
+ const isHTTPRouteConfig = (
16
+ value: ExportedHTTPRouteEntryMeta
17
+ ): value is ExportedHTTPRouteConfigMeta =>
18
+ typeof value === 'object' &&
19
+ value !== null &&
20
+ 'method' in value &&
21
+ 'func' in value &&
22
+ 'route' in value
23
+
24
+ const isHTTPRouteGroup = (
25
+ value: ExportedHTTPRouteEntryMeta
26
+ ): value is ExportedHTTPRoutesGroupMeta =>
27
+ typeof value === 'object' &&
28
+ value !== null &&
29
+ 'routes' in value &&
30
+ !('method' in value)
31
+
32
+ const applyPackageToHTTPRouteMap = (
33
+ routes: ExportedHTTPRouteMapMeta,
34
+ packageName: string,
35
+ namespace?: string
36
+ ) => {
37
+ for (const value of Object.values(routes)) {
38
+ if (!value || typeof value !== 'object') continue
39
+ if (isHTTPRouteConfig(value)) {
40
+ if (!value.func.packageName) {
41
+ value.func.packageName = packageName
42
+ }
43
+ if (namespace && !value.func.pikkuFuncId.includes(':')) {
44
+ value.func.pikkuFuncId = `${namespace}:${value.func.pikkuFuncId}`
45
+ }
46
+ continue
47
+ }
48
+ if (isHTTPRouteGroup(value)) {
49
+ applyPackageToHTTPRouteMap(value.routes, packageName, namespace)
50
+ continue
51
+ }
52
+ applyPackageToHTTPRouteMap(
53
+ value as ExportedHTTPRouteMapMeta,
54
+ packageName,
55
+ namespace
56
+ )
57
+ }
58
+ }
59
+
60
+ const applyPackageToHTTPContracts = (
61
+ contracts: ExportedHTTPContractsMeta,
62
+ packageName: string,
63
+ namespace: string
64
+ ) => {
65
+ for (const contract of Object.values(contracts)) {
66
+ applyPackageToHTTPRouteMap(contract.routes, packageName, namespace)
67
+ }
68
+ }
69
+
70
+ const applyPackageToCLICommands = (
71
+ commands: Record<string, any>,
72
+ packageName: string,
73
+ namespace?: string
74
+ ) => {
75
+ for (const command of Object.values(commands)) {
76
+ if (command && typeof command === 'object') {
77
+ if (!command.packageName && command.pikkuFuncId) {
78
+ command.packageName = packageName
79
+ }
80
+ if (
81
+ namespace &&
82
+ typeof command.pikkuFuncId === 'string' &&
83
+ !command.pikkuFuncId.includes(':')
84
+ ) {
85
+ command.pikkuFuncId = `${namespace}:${command.pikkuFuncId}`
86
+ }
87
+ if (command.subcommands) {
88
+ applyPackageToCLICommands(command.subcommands, packageName, namespace)
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ const applyPackageToCLIContracts = (
95
+ contracts: ExportedCLIContractsMeta,
96
+ packageName: string,
97
+ namespace: string
98
+ ) => {
99
+ for (const commands of Object.values(contracts)) {
100
+ applyPackageToCLICommands(commands, packageName, namespace)
101
+ }
102
+ }
103
+
104
+ const applyPackageToChannelContracts = (
105
+ contracts: ExportedChannelContractsMeta,
106
+ packageName: string,
107
+ namespace: string
108
+ ) => {
109
+ for (const routes of Object.values(contracts)) {
110
+ for (const route of Object.values(routes)) {
111
+ if (!route.packageName) {
112
+ route.packageName = packageName
113
+ }
114
+ if (!route.pikkuFuncId.includes(':')) {
115
+ route.pikkuFuncId = `${namespace}:${route.pikkuFuncId}`
116
+ }
117
+ }
118
+ }
119
+ }
5
120
 
6
121
  /**
7
122
  * After the setup sweep discovers wireAddon() declarations, load each addon
@@ -104,6 +219,55 @@ export async function loadAddonFunctionsMeta(
104
219
  } catch {
105
220
  // No services gen — addon may not have requiredParentServices
106
221
  }
222
+
223
+ try {
224
+ const httpContractsPath = require.resolve(
225
+ `${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`
226
+ )
227
+ const httpContractsRaw = await readFile(httpContractsPath, 'utf-8')
228
+ const httpContracts = JSON.parse(
229
+ httpContractsRaw
230
+ ) as ExportedHTTPContractsMeta
231
+ applyPackageToHTTPContracts(httpContracts, decl.package, namespace)
232
+ state.exportedContracts.addonHttp[namespace] = httpContracts
233
+ } catch {
234
+ // No addon HTTP contracts metadata
235
+ }
236
+
237
+ try {
238
+ const cliContractsPath = require.resolve(
239
+ `${decl.package}/.pikku/cli/pikku-cli-contracts-meta.gen.json`
240
+ )
241
+ const cliContractsRaw = await readFile(cliContractsPath, 'utf-8')
242
+ const cliContracts = JSON.parse(
243
+ cliContractsRaw
244
+ ) as ExportedCLIContractsMeta
245
+ applyPackageToCLIContracts(cliContracts, decl.package, namespace)
246
+ state.exportedContracts.addonCli[namespace] = cliContracts
247
+ } catch {
248
+ // No addon CLI contracts metadata
249
+ }
250
+
251
+ try {
252
+ const channelContractsPath = require.resolve(
253
+ `${decl.package}/.pikku/channel/pikku-channel-contracts-meta.gen.json`
254
+ )
255
+ const channelContractsRaw = await readFile(
256
+ channelContractsPath,
257
+ 'utf-8'
258
+ )
259
+ const channelContracts = JSON.parse(
260
+ channelContractsRaw
261
+ ) as ExportedChannelContractsMeta
262
+ applyPackageToChannelContracts(
263
+ channelContracts,
264
+ decl.package,
265
+ namespace
266
+ )
267
+ state.exportedContracts.addonChannel[namespace] = channelContracts
268
+ } catch {
269
+ // No addon channel contracts metadata
270
+ }
107
271
  } catch (error: any) {
108
272
  logger.warn(
109
273
  `Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`
@@ -80,7 +80,12 @@ export const resolveAddonName = (
80
80
  // Bare package import path
81
81
  if (candidatePackage && !candidatePackage.startsWith('.')) {
82
82
  for (const addonDecl of wireAddonDeclarations.values()) {
83
- if (addonDecl.package === candidatePackage) return addonDecl.package
83
+ if (
84
+ addonDecl.package === candidatePackage ||
85
+ candidatePackage.startsWith(`${addonDecl.package}/`)
86
+ ) {
87
+ return addonDecl.package
88
+ }
84
89
  }
85
90
  }
86
91
 
@@ -0,0 +1,71 @@
1
+ import * as ts from 'typescript'
2
+
3
+ export interface RefContractResolution<T> {
4
+ contract: T
5
+ /**
6
+ * Optional basePath override supplied by the consumer via the second
7
+ * argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
8
+ * the addon contract's own basePath is preserved.
9
+ */
10
+ basePath?: string
11
+ }
12
+
13
+ const getStringProperty = (
14
+ obj: ts.ObjectLiteralExpression,
15
+ name: string
16
+ ): string | undefined => {
17
+ for (const prop of obj.properties) {
18
+ if (
19
+ ts.isPropertyAssignment(prop) &&
20
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
21
+ prop.name.text === name &&
22
+ ts.isStringLiteral(prop.initializer)
23
+ ) {
24
+ return prop.initializer.text
25
+ }
26
+ }
27
+ return undefined
28
+ }
29
+
30
+ /**
31
+ * Resolve a refHTTP / refChannel / refCLI call expression against the addon
32
+ * contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
33
+ *
34
+ * The first string argument has the form 'namespace:contractName', mirroring
35
+ * how ref('namespace:fn') references an addon function. Detection is purely
36
+ * syntactic — no import resolution is required because the namespace and
37
+ * contract name are carried in the string literal. An optional second object
38
+ * argument may override mount details such as basePath.
39
+ */
40
+ export const resolveRefContract = <T>(
41
+ node: ts.Node,
42
+ helperName: 'refHTTP' | 'refChannel' | 'refCLI',
43
+ addonContracts: Record<string, Record<string, T>>
44
+ ): RefContractResolution<T> | null => {
45
+ if (!ts.isCallExpression(node)) return null
46
+ if (
47
+ !ts.isIdentifier(node.expression) ||
48
+ node.expression.text !== helperName
49
+ ) {
50
+ return null
51
+ }
52
+
53
+ const [arg, optionsArg] = node.arguments
54
+ if (!arg || !ts.isStringLiteral(arg)) return null
55
+
56
+ const separator = arg.text.indexOf(':')
57
+ if (separator === -1) return null
58
+
59
+ const namespace = arg.text.slice(0, separator)
60
+ const contractName = arg.text.slice(separator + 1)
61
+
62
+ const contract = addonContracts[namespace]?.[contractName]
63
+ if (contract === undefined) return null
64
+
65
+ let basePath: string | undefined
66
+ if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
67
+ basePath = getStringProperty(optionsArg, 'basePath')
68
+ }
69
+
70
+ return { contract, basePath }
71
+ }
@@ -43,6 +43,7 @@ function makeLogger() {
43
43
  info: () => {},
44
44
  warn: () => {},
45
45
  error: (msg: string) => errors.push(msg),
46
+ diagnostic: ({ message }: { message: string }) => errors.push(message),
46
47
  critical: (_code: string, msg: string) => errors.push(msg),
47
48
  },
48
49
  errors,
@@ -263,6 +263,7 @@ export interface SerializableInspectorState {
263
263
  openAPISpec: Record<string, any> | null
264
264
  diagnostics: InspectorDiagnostic[]
265
265
  addonFunctions: InspectorState['addonFunctions']
266
+ exportedContracts: InspectorState['exportedContracts']
266
267
  }
267
268
 
268
269
  /**
@@ -443,6 +444,7 @@ export function serializeInspectorState(
443
444
  openAPISpec: state.openAPISpec,
444
445
  diagnostics: state.diagnostics,
445
446
  addonFunctions: state.addonFunctions,
447
+ exportedContracts: state.exportedContracts,
446
448
  }
447
449
  }
448
450
 
@@ -630,6 +632,14 @@ export function deserializeInspectorState(
630
632
  openAPISpec: data.openAPISpec || null,
631
633
  diagnostics: data.diagnostics || [],
632
634
  addonFunctions: data.addonFunctions || {},
635
+ exportedContracts: data.exportedContracts || {
636
+ http: {},
637
+ cli: {},
638
+ channel: {},
639
+ addonHttp: {},
640
+ addonCli: {},
641
+ addonChannel: {},
642
+ },
633
643
  program: null,
634
644
  }
635
645
  }
@@ -386,6 +386,22 @@ function extractVariableDeclaration(
386
386
  return step
387
387
  }
388
388
  }
389
+
390
+ // Promise.all fanout/group captured into a variable
391
+ // (const results = await Promise.all(array.map(...)))
392
+ if (isParallelFanout(call) || isParallelGroup(call)) {
393
+ const step = isParallelFanout(call)
394
+ ? extractParallelFanout(call, context)
395
+ : extractParallelGroup(call, context)
396
+ if (step) {
397
+ const type = context.checker.getTypeAtLocation(decl)
398
+ context.outputVars.set(varName, { type, node: decl })
399
+ if (isArrayType(type, context.checker)) {
400
+ context.arrayVars.add(varName)
401
+ }
402
+ return step
403
+ }
404
+ }
389
405
  }
390
406
 
391
407
  // Check for array.filter(...)
package/src/visit.ts 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'
@@ -106,27 +107,33 @@ export const visitRoutes = (
106
107
  state: InspectorState,
107
108
  options: InspectorOptions
108
109
  ) => {
109
- addFunctions(logger, node, checker, state, options)
110
- addAuth(logger, node, checker, state, options)
111
- addSecret(logger, node, checker, state, options)
112
- addCredential(logger, node, checker, state, options)
113
- addVariable(logger, node, checker, state, options)
110
+ const nextOptions = ts.isSourceFile(node)
111
+ ? { ...options, sourceFile: node }
112
+ : options
114
113
 
115
- addHTTPRoute(logger, node, checker, state, options)
116
- addHTTPRoutes(logger, node, checker, state, options)
117
- addSchedule(logger, node, checker, state, options)
118
- addTrigger(logger, node, checker, state, options)
119
- addQueueWorker(logger, node, checker, state, options)
120
- addChannel(logger, node, checker, state, options)
121
- addGateway(logger, node, checker, state, options)
122
- addCLI(logger, node, checker, state, options)
123
- addCLIRenderers(logger, node, checker, state, options)
124
- addMCPResource(logger, node, checker, state, options)
125
- addMCPPrompt(logger, node, checker, state, options)
126
- addWorkflowGraph(logger, node, checker, state, options)
127
- addAIAgent(logger, node, checker, state, options)
114
+ checkAddonBans(logger, node, checker, state, nextOptions)
115
+
116
+ addFunctions(logger, node, checker, state, nextOptions)
117
+ addAuth(logger, node, checker, state, nextOptions)
118
+ addSecret(logger, node, checker, state, nextOptions)
119
+ addCredential(logger, node, checker, state, nextOptions)
120
+ addVariable(logger, node, checker, state, nextOptions)
121
+
122
+ addHTTPRoute(logger, node, checker, state, nextOptions)
123
+ addHTTPRoutes(logger, node, checker, state, nextOptions)
124
+ addSchedule(logger, node, checker, state, nextOptions)
125
+ addTrigger(logger, node, checker, state, nextOptions)
126
+ addQueueWorker(logger, node, checker, state, nextOptions)
127
+ addChannel(logger, node, checker, state, nextOptions)
128
+ addGateway(logger, node, checker, state, nextOptions)
129
+ addCLI(logger, node, checker, state, nextOptions)
130
+ addCLIRenderers(logger, node, checker, state, nextOptions)
131
+ addMCPResource(logger, node, checker, state, nextOptions)
132
+ addMCPPrompt(logger, node, checker, state, nextOptions)
133
+ addWorkflowGraph(logger, node, checker, state, nextOptions)
134
+ addAIAgent(logger, node, checker, state, nextOptions)
128
135
 
129
136
  ts.forEachChild(node, (child) =>
130
- visitRoutes(logger, checker, child, state, options)
137
+ visitRoutes(logger, checker, child, state, nextOptions)
131
138
  )
132
139
  }