@pikku/inspector 0.12.22 → 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 (40) hide show
  1. package/CHANGELOG.md +10 -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-http-route.d.ts +11 -1
  7. package/dist/add/add-http-route.js +37 -0
  8. package/dist/add/add-http-routes.d.ts +0 -3
  9. package/dist/add/add-http-routes.js +179 -36
  10. package/dist/error-codes.d.ts +3 -1
  11. package/dist/error-codes.js +3 -0
  12. package/dist/inspector.js +17 -5
  13. package/dist/types.d.ts +43 -1
  14. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  15. package/dist/utils/get-exported-variable-name.js +34 -0
  16. package/dist/utils/load-addon-functions-meta.js +98 -0
  17. package/dist/utils/resolve-addon-package.js +3 -1
  18. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  19. package/dist/utils/resolve-ref-contract.js +46 -0
  20. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  21. package/dist/utils/serialize-inspector-state.js +9 -0
  22. package/dist/visit.js +24 -19
  23. package/package.json +1 -1
  24. package/src/add/add-addon-bans.ts +84 -0
  25. package/src/add/add-channel.ts +66 -7
  26. package/src/add/add-cli.ts +30 -0
  27. package/src/add/add-http-route.ts +75 -1
  28. package/src/add/add-http-routes.ts +283 -41
  29. package/src/add/addon-bans.test.ts +121 -0
  30. package/src/add/addon-contracts.test.ts +221 -0
  31. package/src/error-codes.ts +4 -0
  32. package/src/inspector.ts +17 -5
  33. package/src/types.ts +65 -1
  34. package/src/utils/get-exported-variable-name.ts +48 -0
  35. package/src/utils/load-addon-functions-meta.ts +164 -0
  36. package/src/utils/resolve-addon-package.ts +6 -1
  37. package/src/utils/resolve-ref-contract.ts +71 -0
  38. package/src/utils/serialize-inspector-state.ts +10 -0
  39. package/src/visit.ts +26 -19
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,121 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const recordingLogger = () => {
11
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
12
+ const logger: InspectorLogger = {
13
+ debug: () => {},
14
+ info: () => {},
15
+ warn: () => {},
16
+ error: () => {},
17
+ critical: (code, message) => {
18
+ criticals.push({ code, message })
19
+ },
20
+ hasCriticalErrors: () => criticals.length > 0,
21
+ }
22
+ return { logger, criticals }
23
+ }
24
+
25
+ const withTempApp = async (
26
+ source: string,
27
+ run: (file: string, rootDir: string) => Promise<void>
28
+ ) => {
29
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-addon-bans-'))
30
+ const file = join(rootDir, 'app.ts')
31
+ await writeFile(
32
+ join(rootDir, 'package.json'),
33
+ JSON.stringify({ name: 'test-addon', type: 'module' }, null, 2)
34
+ )
35
+ await writeFile(file, source)
36
+ try {
37
+ await run(file, rootDir)
38
+ } finally {
39
+ await rm(rootDir, { recursive: true, force: true })
40
+ }
41
+ }
42
+
43
+ describe('addon authoring bans', () => {
44
+ const wireHTTPSource = [
45
+ "import { wireHTTP } from '@pikku/core/http'",
46
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
47
+ 'const f = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
48
+ "wireHTTP({ method: 'get', route: '/a', func: f })",
49
+ ].join('\n')
50
+
51
+ const defineWithMiddlewareSource = [
52
+ "import { defineHTTPRoutes } from '@pikku/core/http'",
53
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
54
+ 'const f = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
55
+ 'const mw = (async (_s: any, _w: any, next: any) => next()) as any',
56
+ "export const routes = defineHTTPRoutes({ basePath: '/x', routes: { a: { method: 'get', route: '/a', func: f, middleware: [mw] } } })",
57
+ ].join('\n')
58
+
59
+ test('wire* inside an addon is a critical error', async () => {
60
+ await withTempApp(wireHTTPSource, async (file, rootDir) => {
61
+ const { logger, criticals } = recordingLogger()
62
+ await inspect(logger, [file], { rootDir, isAddon: true })
63
+ assert.ok(
64
+ criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
65
+ `expected ADDON_WIRING_NOT_ALLOWED, got ${JSON.stringify(criticals)}`
66
+ )
67
+ })
68
+ })
69
+
70
+ test('wire* outside an addon is allowed', async () => {
71
+ await withTempApp(wireHTTPSource, async (file, rootDir) => {
72
+ const { logger, criticals } = recordingLogger()
73
+ await inspect(logger, [file], { rootDir })
74
+ assert.ok(
75
+ !criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
76
+ `expected no addon-wiring ban, got ${JSON.stringify(criticals)}`
77
+ )
78
+ })
79
+ })
80
+
81
+ test('define* carrying middleware inside an addon is a critical error', async () => {
82
+ await withTempApp(defineWithMiddlewareSource, async (file, rootDir) => {
83
+ const { logger, criticals } = recordingLogger()
84
+ await inspect(logger, [file], { rootDir, isAddon: true })
85
+ assert.ok(
86
+ criticals.some(
87
+ (c) => c.code === ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED
88
+ ),
89
+ `expected ADDON_CONTRACT_HANDLERS_NOT_ALLOWED, got ${JSON.stringify(criticals)}`
90
+ )
91
+ })
92
+ })
93
+
94
+ test('define* carrying middleware outside an addon is allowed', async () => {
95
+ await withTempApp(defineWithMiddlewareSource, async (file, rootDir) => {
96
+ const { logger, criticals } = recordingLogger()
97
+ await inspect(logger, [file], { rootDir })
98
+ assert.ok(
99
+ !criticals.some(
100
+ (c) => c.code === ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED
101
+ ),
102
+ `expected no contract-handlers ban, got ${JSON.stringify(criticals)}`
103
+ )
104
+ })
105
+ })
106
+
107
+ test('wireSecret inside an addon is allowed', async () => {
108
+ const source = [
109
+ "import { wireSecret } from '@pikku/core'",
110
+ "wireSecret({ name: 'example', secretId: 'EXAMPLE' } as any)",
111
+ ].join('\n')
112
+ await withTempApp(source, async (file, rootDir) => {
113
+ const { logger, criticals } = recordingLogger()
114
+ await inspect(logger, [file], { rootDir, isAddon: true })
115
+ assert.ok(
116
+ !criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
117
+ `expected wireSecret to be allowed, got ${JSON.stringify(criticals)}`
118
+ )
119
+ })
120
+ })
121
+ })
@@ -0,0 +1,221 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import type { InspectorLogger } from '../types.js'
8
+
9
+ const logger: InspectorLogger = {
10
+ debug: () => {},
11
+ info: () => {},
12
+ warn: () => {},
13
+ error: () => {},
14
+ critical: () => {},
15
+ hasCriticalErrors: () => false,
16
+ }
17
+
18
+ describe('addon contract metadata', () => {
19
+ test('collects exported local define contracts', async () => {
20
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-exported-contracts-'))
21
+ const file = join(rootDir, 'contracts.ts')
22
+
23
+ await writeFile(
24
+ join(rootDir, 'package.json'),
25
+ JSON.stringify({ name: 'test-app', type: 'module' }, null, 2)
26
+ )
27
+
28
+ await writeFile(
29
+ file,
30
+ [
31
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
32
+ "import { defineHTTPRoutes } from '@pikku/core/http'",
33
+ "import { defineChannelRoutes } from '@pikku/core/channel'",
34
+ "import { defineCLICommands } from '@pikku/core/cli'",
35
+ 'export const listTodos = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
36
+ 'export const subscribeTodo = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
37
+ 'export const runSync = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
38
+ "export const httpRoutes = defineHTTPRoutes({ basePath: '/todos', routes: { list: { method: 'get', route: '/', func: listTodos } } })",
39
+ 'export const channelRoutes = defineChannelRoutes({ subscribe: { func: subscribeTodo } })',
40
+ 'export const cliCommands = defineCLICommands({ sync: { func: runSync, options: {} } })',
41
+ ].join('\n')
42
+ )
43
+
44
+ try {
45
+ const state = await inspect(logger, [file], { rootDir })
46
+ assert.equal(
47
+ state.exportedContracts.http.httpRoutes?.routes.list?.func.pikkuFuncId,
48
+ 'listTodos'
49
+ )
50
+ assert.equal(
51
+ state.exportedContracts.channel.channelRoutes?.subscribe?.pikkuFuncId,
52
+ 'subscribeTodo'
53
+ )
54
+ assert.equal(
55
+ state.exportedContracts.cli.cliCommands?.sync?.pikkuFuncId,
56
+ 'runSync'
57
+ )
58
+ } finally {
59
+ await rm(rootDir, { recursive: true, force: true })
60
+ }
61
+ })
62
+
63
+ test('expands addon contracts referenced via refHTTP/refChannel/refCLI', async () => {
64
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-addon-refs-app-'))
65
+ const nodeModulesDir = join(rootDir, 'node_modules', '@test', 'addon')
66
+ const appFile = join(rootDir, 'app.ts')
67
+
68
+ await mkdir(join(nodeModulesDir, '.pikku', 'function'), { recursive: true })
69
+ await mkdir(join(nodeModulesDir, '.pikku', 'http'), { recursive: true })
70
+ await mkdir(join(nodeModulesDir, '.pikku', 'cli'), { recursive: true })
71
+ await mkdir(join(nodeModulesDir, '.pikku', 'channel'), { recursive: true })
72
+
73
+ await writeFile(
74
+ join(rootDir, 'package.json'),
75
+ JSON.stringify({ name: 'test-app', type: 'module' }, null, 2)
76
+ )
77
+ await writeFile(
78
+ join(nodeModulesDir, 'package.json'),
79
+ JSON.stringify({ name: '@test/addon', type: 'module' }, null, 2)
80
+ )
81
+ await writeFile(
82
+ join(
83
+ nodeModulesDir,
84
+ '.pikku',
85
+ 'function',
86
+ 'pikku-functions-meta.gen.json'
87
+ ),
88
+ JSON.stringify(
89
+ {
90
+ listTodos: {
91
+ pikkuFuncId: 'listTodos',
92
+ inputSchemaName: null,
93
+ outputSchemaName: null,
94
+ sessionless: true,
95
+ },
96
+ runSync: {
97
+ pikkuFuncId: 'runSync',
98
+ inputSchemaName: null,
99
+ outputSchemaName: null,
100
+ sessionless: true,
101
+ },
102
+ subscribeTodo: {
103
+ pikkuFuncId: 'subscribeTodo',
104
+ inputSchemaName: null,
105
+ outputSchemaName: null,
106
+ sessionless: true,
107
+ },
108
+ },
109
+ null,
110
+ 2
111
+ )
112
+ )
113
+ await writeFile(
114
+ join(
115
+ nodeModulesDir,
116
+ '.pikku',
117
+ 'http',
118
+ 'pikku-http-contracts-meta.gen.json'
119
+ ),
120
+ JSON.stringify(
121
+ {
122
+ httpRoutes: {
123
+ basePath: '/addon',
124
+ routes: {
125
+ list: {
126
+ method: 'get',
127
+ route: '/todos',
128
+ func: { pikkuFuncId: 'listTodos' },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ null,
134
+ 2
135
+ )
136
+ )
137
+ await writeFile(
138
+ join(
139
+ nodeModulesDir,
140
+ '.pikku',
141
+ 'cli',
142
+ 'pikku-cli-contracts-meta.gen.json'
143
+ ),
144
+ JSON.stringify(
145
+ {
146
+ cliCommands: {
147
+ sync: {
148
+ pikkuFuncId: 'runSync',
149
+ positionals: [],
150
+ options: {},
151
+ },
152
+ },
153
+ },
154
+ null,
155
+ 2
156
+ )
157
+ )
158
+ await writeFile(
159
+ join(
160
+ nodeModulesDir,
161
+ '.pikku',
162
+ 'channel',
163
+ 'pikku-channel-contracts-meta.gen.json'
164
+ ),
165
+ JSON.stringify(
166
+ {
167
+ channelRoutes: {
168
+ subscribe: {
169
+ pikkuFuncId: 'subscribeTodo',
170
+ },
171
+ },
172
+ },
173
+ null,
174
+ 2
175
+ )
176
+ )
177
+
178
+ await writeFile(
179
+ appFile,
180
+ [
181
+ "import { wireAddon } from '@pikku/core/rpc'",
182
+ "import { wireHTTPRoutes } from '@pikku/core/http'",
183
+ "import { wireChannel } from '@pikku/core/channel'",
184
+ "import { wireCLI } from '@pikku/core/cli'",
185
+ 'declare const refHTTP: (name: string) => any',
186
+ 'declare const refChannel: (name: string) => any',
187
+ 'declare const refCLI: (name: string) => any',
188
+ "wireAddon({ name: 'addon', package: '@test/addon' })",
189
+ "wireHTTPRoutes({ basePath: '/api', routes: { keep: refHTTP('addon:httpRoutes'), moved: refHTTP('addon:httpRoutes', { basePath: '/ext' }) } })",
190
+ "wireChannel({ name: 'live', route: '/live', auth: false, onMessageWiring: { action: refChannel('addon:channelRoutes') } })",
191
+ "wireCLI({ program: 'app', commands: { ...refCLI('addon:cliCommands') } })",
192
+ ].join('\n')
193
+ )
194
+
195
+ try {
196
+ const state = await inspect(logger, [appFile], { rootDir })
197
+
198
+ // Default: the addon contract's own basePath ('/addon') is preserved —
199
+ // the cosmetic slot key ('keep') does not affect the path.
200
+ const httpEntry = state.http.meta.get['/api/addon/todos']
201
+ assert.equal(httpEntry?.packageName, '@test/addon')
202
+ assert.equal(httpEntry?.pikkuFuncId, 'addon:listTodos')
203
+
204
+ // Override: the second-arg basePath ('/ext') replaces the addon's own.
205
+ const overridden = state.http.meta.get['/api/ext/todos']
206
+ assert.equal(overridden?.packageName, '@test/addon')
207
+ assert.equal(overridden?.pikkuFuncId, 'addon:listTodos')
208
+
209
+ const channelEntry =
210
+ state.channels.meta.live?.messageWirings.action?.subscribe
211
+ assert.equal(channelEntry?.packageName, '@test/addon')
212
+ assert.equal(channelEntry?.pikkuFuncId, 'addon:subscribeTodo')
213
+
214
+ const cliEntry = state.cli.meta.programs.app?.commands.sync
215
+ assert.equal(cliEntry?.packageName, '@test/addon')
216
+ assert.equal(cliEntry?.pikkuFuncId, 'addon:runSync')
217
+ } finally {
218
+ await rm(rootDir, { recursive: true, force: true })
219
+ }
220
+ })
221
+ })
@@ -84,6 +84,10 @@ export enum ErrorCode {
84
84
 
85
85
  // Data classification errors
86
86
  PII_IN_OUTPUT = 'PKU910',
87
+
88
+ // Addon authoring errors
89
+ ADDON_WIRING_NOT_ALLOWED = 'PKU920',
90
+ ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = 'PKU921',
87
91
  }
88
92
 
89
93
  /**
package/src/inspector.ts 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 type {
@@ -216,6 +217,14 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
216
217
  openAPISpec: null,
217
218
  diagnostics: [],
218
219
  addonFunctions: {},
220
+ exportedContracts: {
221
+ http: {},
222
+ cli: {},
223
+ channel: {},
224
+ addonHttp: {},
225
+ addonCli: {},
226
+ addonChannel: {},
227
+ },
219
228
  program: null,
220
229
  }
221
230
  }
@@ -225,6 +234,7 @@ export const inspect = async (
225
234
  routeFiles: string[],
226
235
  options: InspectorOptions = {}
227
236
  ): Promise<InspectorState> => {
237
+ const normalizedRouteFiles = routeFiles.map((file) => resolve(file))
228
238
  const compilerOptions: ts.CompilerOptions = {
229
239
  target: ts.ScriptTarget.ESNext,
230
240
  module: ts.ModuleKind.Node16,
@@ -237,13 +247,13 @@ export const inspect = async (
237
247
  }
238
248
  const startProgram = performance.now()
239
249
  const program = ts.createProgram(
240
- routeFiles,
250
+ normalizedRouteFiles,
241
251
  compilerOptions,
242
252
  undefined, // host
243
253
  options.oldProgram
244
254
  )
245
255
  logger.debug(
246
- `Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`
256
+ `Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files${options.oldProgram ? ', incremental' : ''})`
247
257
  )
248
258
 
249
259
  const startChecker = performance.now()
@@ -253,7 +263,7 @@ export const inspect = async (
253
263
  )
254
264
 
255
265
  // Use provided rootDir or infer from source files
256
- const rootDir = options.rootDir || findCommonAncestor(routeFiles)
266
+ const rootDir = options.rootDir || findCommonAncestor(normalizedRouteFiles)
257
267
 
258
268
  const startSourceFiles = performance.now()
259
269
  // node_modules under rootDir (e.g. a locally-installed addon) is a
@@ -275,8 +285,9 @@ export const inspect = async (
275
285
  // First sweep: add all functions
276
286
  const startSetup = performance.now()
277
287
  for (const sourceFile of sourceFiles) {
288
+ const sourceOptions = { ...options, sourceFile }
278
289
  ts.forEachChild(sourceFile, (child) =>
279
- visitSetup(logger, checker, child, state, options)
290
+ visitSetup(logger, checker, child, state, sourceOptions)
280
291
  )
281
292
  }
282
293
  logger.debug(
@@ -290,8 +301,9 @@ export const inspect = async (
290
301
  // Second sweep: add all transports
291
302
  const startRoutes = performance.now()
292
303
  for (const sourceFile of sourceFiles) {
304
+ const sourceOptions = { ...options, sourceFile }
293
305
  ts.forEachChild(sourceFile, (child) =>
294
- visitRoutes(logger, checker, child, state, options)
306
+ visitRoutes(logger, checker, child, state, sourceOptions)
295
307
  )
296
308
  }
297
309
  logger.debug(
package/src/types.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'
@@ -13,6 +13,7 @@ import type {
13
13
  } from '@pikku/core/mcp'
14
14
  import type { AIAgentMeta } from '@pikku/core/ai-agent'
15
15
  import type { CLIMeta } from '@pikku/core/cli'
16
+ import type { CLICommandMeta } from '@pikku/core/cli'
16
17
  import type { NodesMeta } from '@pikku/core/node'
17
18
  import type { SecretDefinitions } from '@pikku/core/secret'
18
19
  import type { CredentialDefinitions } from '@pikku/core/credential'
@@ -107,6 +108,67 @@ export interface InspectorChannelState {
107
108
  files: Set<string>
108
109
  }
109
110
 
111
+ export interface ExportedHTTPRouteFunctionMeta {
112
+ pikkuFuncId: string
113
+ packageName?: string
114
+ }
115
+
116
+ export interface ExportedHTTPRouteConfigMeta {
117
+ method: string
118
+ route: string
119
+ func: ExportedHTTPRouteFunctionMeta
120
+ auth?: boolean
121
+ tags?: string[]
122
+ sse?: boolean
123
+ contentType?: string
124
+ timeout?: number
125
+ headers?: Record<string, string>
126
+ }
127
+
128
+ export interface ExportedHTTPRoutesGroupMeta {
129
+ basePath?: string
130
+ tags?: string[]
131
+ auth?: boolean
132
+ routes: ExportedHTTPRouteMapMeta
133
+ }
134
+
135
+ export type ExportedHTTPRouteEntryMeta =
136
+ | ExportedHTTPRouteConfigMeta
137
+ | ExportedHTTPRoutesGroupMeta
138
+ | ExportedHTTPRouteMapMeta
139
+
140
+ export interface ExportedHTTPRouteMapMeta {
141
+ [key: string]: ExportedHTTPRouteEntryMeta
142
+ }
143
+
144
+ export type ExportedHTTPContractsMeta = Record<
145
+ string,
146
+ ExportedHTTPRoutesGroupMeta
147
+ >
148
+
149
+ export interface ExportedChannelRouteMeta extends ChannelMessageMeta {
150
+ auth?: boolean
151
+ }
152
+
153
+ export type ExportedChannelContractsMeta = Record<
154
+ string,
155
+ Record<string, ExportedChannelRouteMeta>
156
+ >
157
+
158
+ export type ExportedCLIContractsMeta = Record<
159
+ string,
160
+ Record<string, CLICommandMeta>
161
+ >
162
+
163
+ export interface InspectorExportedContractsState {
164
+ http: ExportedHTTPContractsMeta
165
+ cli: ExportedCLIContractsMeta
166
+ channel: ExportedChannelContractsMeta
167
+ addonHttp: Record<string, ExportedHTTPContractsMeta>
168
+ addonCli: Record<string, ExportedCLIContractsMeta>
169
+ addonChannel: Record<string, ExportedChannelContractsMeta>
170
+ }
171
+
110
172
  export interface InspectorMiddlewareDefinition {
111
173
  services: FunctionServicesMeta
112
174
  wires?: FunctionWiresMeta
@@ -214,6 +276,7 @@ export type InspectorOptions = Partial<{
214
276
  setupOnly: boolean
215
277
  rootDir: string
216
278
  isAddon: boolean
279
+ sourceFile: ts.SourceFile
217
280
  types: Partial<{
218
281
  configFileType: string
219
282
  userSessionType: string
@@ -485,5 +548,6 @@ export interface InspectorState {
485
548
  openAPISpec: Record<string, any> | null
486
549
  diagnostics: InspectorDiagnostic[]
487
550
  addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
551
+ exportedContracts: InspectorExportedContractsState
488
552
  program: ts.Program | null // Retained for incremental re-inspection
489
553
  }
@@ -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
+ }