@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.
- package/CHANGELOG.md +40 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-auth.js +43 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- package/dist/add/add-http-route.d.ts +11 -1
- package/dist/add/add-http-route.js +37 -0
- package/dist/add/add-http-routes.d.ts +0 -3
- package/dist/add/add-http-routes.js +179 -36
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/inspector.js +17 -5
- package/dist/types.d.ts +48 -1
- package/dist/utils/get-exported-variable-name.d.ts +2 -0
- package/dist/utils/get-exported-variable-name.js +34 -0
- package/dist/utils/load-addon-functions-meta.js +98 -0
- package/dist/utils/post-process.js +16 -3
- package/dist/utils/resolve-addon-package.js +3 -1
- package/dist/utils/resolve-ref-contract.d.ts +21 -0
- package/dist/utils/resolve-ref-contract.js +46 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +9 -0
- package/dist/visit.js +24 -19
- package/package.json +1 -1
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-auth.test.ts +94 -0
- package/src/add/add-auth.ts +46 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/error-codes.ts +4 -0
- package/src/inspector.ts +17 -5
- package/src/types.ts +70 -1
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/post-process.ts +17 -3
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/visit.ts +26 -19
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
+
})
|
package/src/error-codes.ts
CHANGED
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
|
-
|
|
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 (${
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
@@ -434,6 +497,11 @@ export interface InspectorState {
|
|
|
434
497
|
* codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
|
|
435
498
|
* More than one `pikkuBetterAuth` is a critical error. */
|
|
436
499
|
definition: AuthDefinition | null
|
|
500
|
+
/** True when a user (non-generated) file already registers
|
|
501
|
+
* `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
|
|
502
|
+
* own default-map stateless middleware, which would otherwise pre-empt the
|
|
503
|
+
* user's custom mapSession (pikkujs/pikku#754). */
|
|
504
|
+
userStatelessSession?: boolean
|
|
437
505
|
}
|
|
438
506
|
secrets: {
|
|
439
507
|
definitions: SecretDefinitions
|
|
@@ -485,5 +553,6 @@ export interface InspectorState {
|
|
|
485
553
|
openAPISpec: Record<string, any> | null
|
|
486
554
|
diagnostics: InspectorDiagnostic[]
|
|
487
555
|
addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
|
|
556
|
+
exportedContracts: InspectorExportedContractsState
|
|
488
557
|
program: ts.Program | null // Retained for incremental re-inspection
|
|
489
558
|
}
|
|
@@ -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}`
|
|
@@ -317,9 +317,23 @@ export function aggregateRequiredServices(
|
|
|
317
317
|
requiredServices.add('eventHub')
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// 7. Services that addons need from the parent project
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
// 7. Services that consumed addons need from the parent project.
|
|
321
|
+
// These are required ONLY by units that actually deploy an addon function;
|
|
322
|
+
// a unit that merely calls the addon over RPC (or never touches it) must not
|
|
323
|
+
// carry them, or every per-unit bundle would over-include the addon's
|
|
324
|
+
// parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
|
|
325
|
+
// defeat per-unit tree-shaking.
|
|
326
|
+
const addonFuncIds = new Set<string>()
|
|
327
|
+
for (const fns of Object.values(state.addonFunctions ?? {})) {
|
|
328
|
+
for (const id of Object.keys(fns)) addonFuncIds.add(id)
|
|
329
|
+
}
|
|
330
|
+
const unitDeploysAddonFn = [...usedFunctions].some((fn) =>
|
|
331
|
+
addonFuncIds.has(fn)
|
|
332
|
+
)
|
|
333
|
+
if (unitDeploysAddonFn) {
|
|
334
|
+
for (const service of state.addonRequiredParentServices ?? []) {
|
|
335
|
+
requiredServices.add(service)
|
|
336
|
+
}
|
|
323
337
|
}
|
|
324
338
|
}
|
|
325
339
|
|
|
@@ -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 (
|
|
83
|
+
if (
|
|
84
|
+
addonDecl.package === candidatePackage ||
|
|
85
|
+
candidatePackage.startsWith(`${addonDecl.package}/`)
|
|
86
|
+
) {
|
|
87
|
+
return addonDecl.package
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|