@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.
- package/CHANGELOG.md +30 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- package/dist/add/add-functions.js +16 -8
- 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/add/add-workflow.js +16 -2
- package/dist/error-codes.d.ts +15 -1
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/inspector.js +22 -6
- package/dist/types.d.ts +53 -2
- package/dist/utils/extract-node-value.js +19 -2
- 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/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/utils/workflow/dsl/extract-dsl-workflow.js +15 -0
- package/dist/visit.js +24 -19
- package/package.json +2 -2
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-auth.test.ts +3 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli-renderers.test.ts +1 -0
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-functions.test.ts +13 -0
- package/src/add/add-functions.ts +14 -10
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/add-workflow-fanout.test.ts +106 -0
- package/src/add/add-workflow.test.ts +3 -0
- package/src/add/add-workflow.ts +16 -2
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/add/pii-check.test.ts +4 -0
- package/src/add/wire-name-literal.test.ts +3 -0
- package/src/error-codes.ts +18 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +25 -6
- package/src/types.ts +75 -2
- package/src/utils/extract-node-value.test.ts +49 -1
- package/src/utils/extract-node-value.ts +19 -2
- package/src/utils/filter-inspector-state.test.ts +1 -0
- package/src/utils/filter-utils.test.ts +1 -0
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/resolve-versions.test.ts +1 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +16 -0
- package/src/visit.ts +26 -19
- 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
|
+
})
|
|
@@ -10,12 +10,16 @@ import type { InspectorLogger } from '../types.js'
|
|
|
10
10
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
function makeLogger() {
|
|
13
|
+
// Collects every coded diagnostic regardless of severity. PKU910 is now
|
|
14
|
+
// emitted at 'error' severity (surface, don't block the dev server) so it
|
|
15
|
+
// arrives via `diagnostic`, not `critical`.
|
|
13
16
|
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
14
17
|
const logger: InspectorLogger = {
|
|
15
18
|
debug: () => {},
|
|
16
19
|
info: () => {},
|
|
17
20
|
warn: () => {},
|
|
18
21
|
error: () => {},
|
|
22
|
+
diagnostic: ({ code, message }) => criticals.push({ code, message }),
|
|
19
23
|
critical: (code, message) => criticals.push({ code, message }),
|
|
20
24
|
hasCriticalErrors: () => criticals.length > 0,
|
|
21
25
|
}
|
|
@@ -13,6 +13,9 @@ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
|
13
13
|
info: () => {},
|
|
14
14
|
warn: () => {},
|
|
15
15
|
error: () => {},
|
|
16
|
+
diagnostic: ({ code, message }) => {
|
|
17
|
+
criticals.push({ code, message })
|
|
18
|
+
},
|
|
16
19
|
critical: (code: ErrorCode, message: string) => {
|
|
17
20
|
criticals.push({ code, message })
|
|
18
21
|
},
|
package/src/error-codes.ts
CHANGED
|
@@ -84,4 +84,22 @@ 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',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Severity of a tracked, coded diagnostic. `critical` always blocks the build;
|
|
95
|
+
* `error`/`warn` only block when the CLI is told to via `--fail-on-error` /
|
|
96
|
+
* `--fail-on-warn` (default: critical only). All severities are still printed.
|
|
97
|
+
*/
|
|
98
|
+
export type DiagnosticSeverity = 'warn' | 'error' | 'critical'
|
|
99
|
+
|
|
100
|
+
/** A coded diagnostic emitted via `logger.diagnostic(...)`. */
|
|
101
|
+
export interface CodedDiagnostic {
|
|
102
|
+
severity: DiagnosticSeverity
|
|
103
|
+
code: ErrorCode
|
|
104
|
+
message: string
|
|
87
105
|
}
|
package/src/index.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 {
|
|
8
9
|
serializeInspectorState,
|
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,12 +263,19 @@ 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()
|
|
269
|
+
// node_modules under rootDir (e.g. a locally-installed addon) is a
|
|
270
|
+
// dependency, not project source — scanning it double-counts the addon's
|
|
271
|
+
// own application types (CoreConfig/Services/SingletonServices).
|
|
259
272
|
const sourceFiles = program
|
|
260
273
|
.getSourceFiles()
|
|
261
|
-
.filter(
|
|
274
|
+
.filter(
|
|
275
|
+
(sf) =>
|
|
276
|
+
sf.fileName.startsWith(rootDir) &&
|
|
277
|
+
!sf.fileName.includes('/node_modules/')
|
|
278
|
+
)
|
|
262
279
|
logger.debug(
|
|
263
280
|
`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`
|
|
264
281
|
)
|
|
@@ -268,8 +285,9 @@ export const inspect = async (
|
|
|
268
285
|
// First sweep: add all functions
|
|
269
286
|
const startSetup = performance.now()
|
|
270
287
|
for (const sourceFile of sourceFiles) {
|
|
288
|
+
const sourceOptions = { ...options, sourceFile }
|
|
271
289
|
ts.forEachChild(sourceFile, (child) =>
|
|
272
|
-
visitSetup(logger, checker, child, state,
|
|
290
|
+
visitSetup(logger, checker, child, state, sourceOptions)
|
|
273
291
|
)
|
|
274
292
|
}
|
|
275
293
|
logger.debug(
|
|
@@ -283,8 +301,9 @@ export const inspect = async (
|
|
|
283
301
|
// Second sweep: add all transports
|
|
284
302
|
const startRoutes = performance.now()
|
|
285
303
|
for (const sourceFile of sourceFiles) {
|
|
304
|
+
const sourceOptions = { ...options, sourceFile }
|
|
286
305
|
ts.forEachChild(sourceFile, (child) =>
|
|
287
|
-
visitRoutes(logger, checker, child, state,
|
|
306
|
+
visitRoutes(logger, checker, child, state, sourceOptions)
|
|
288
307
|
)
|
|
289
308
|
}
|
|
290
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'
|
|
@@ -25,7 +26,7 @@ import type {
|
|
|
25
26
|
JSONValue,
|
|
26
27
|
} from '@pikku/core'
|
|
27
28
|
import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js'
|
|
28
|
-
import type { ErrorCode } from './error-codes.js'
|
|
29
|
+
import type { ErrorCode, CodedDiagnostic } from './error-codes.js'
|
|
29
30
|
import type {
|
|
30
31
|
VersionManifest,
|
|
31
32
|
VersionValidateError,
|
|
@@ -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
|
|
@@ -238,6 +301,15 @@ export interface InspectorLogger {
|
|
|
238
301
|
error: (message: string) => void
|
|
239
302
|
warn: (message: string) => void
|
|
240
303
|
debug: (message: string) => void
|
|
304
|
+
/**
|
|
305
|
+
* Emit a tracked, coded diagnostic. It is recorded and printed; `error`/`warn`
|
|
306
|
+
* only block the build when the CLI is run with `--fail-on-error` /
|
|
307
|
+
* `--fail-on-warn` (default: critical only). Use this for issues worth
|
|
308
|
+
* surfacing (e.g. data-classification leaks) that should not stop the dev
|
|
309
|
+
* server from starting.
|
|
310
|
+
*/
|
|
311
|
+
diagnostic: (diagnostic: CodedDiagnostic) => void
|
|
312
|
+
/** Sugar for `diagnostic({ severity: 'critical', code, message })`. */
|
|
241
313
|
critical: (code: ErrorCode, message: string) => void
|
|
242
314
|
hasCriticalErrors: () => boolean
|
|
243
315
|
}
|
|
@@ -476,5 +548,6 @@ export interface InspectorState {
|
|
|
476
548
|
openAPISpec: Record<string, any> | null
|
|
477
549
|
diagnostics: InspectorDiagnostic[]
|
|
478
550
|
addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
|
|
551
|
+
exportedContracts: InspectorExportedContractsState
|
|
479
552
|
program: ts.Program | null // Retained for incremental re-inspection
|
|
480
553
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, describe } from 'node:test'
|
|
2
2
|
import { strict as assert } from 'node:assert'
|
|
3
3
|
import * as ts from 'typescript'
|
|
4
|
-
import { extractDescription } from './extract-node-value'
|
|
4
|
+
import { extractDescription, extractStringLiteral } from './extract-node-value'
|
|
5
5
|
|
|
6
6
|
const createChecker = (source: string) => {
|
|
7
7
|
const sourceFile = ts.createSourceFile(
|
|
@@ -67,3 +67,51 @@ describe('extractDescription', () => {
|
|
|
67
67
|
assert.equal(extractDescription(sourceFile, checker), null)
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
|
+
|
|
71
|
+
describe('extractStringLiteral — concatenation/template symmetry', () => {
|
|
72
|
+
const findInitializer = (node: ts.Node): ts.Expression | undefined => {
|
|
73
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
74
|
+
return node.initializer
|
|
75
|
+
}
|
|
76
|
+
let result: ts.Expression | undefined
|
|
77
|
+
ts.forEachChild(node, (child) => {
|
|
78
|
+
if (!result) result = findInitializer(child)
|
|
79
|
+
})
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('a `+` operand that cannot be statically resolved becomes a ${...} placeholder', () => {
|
|
84
|
+
const { checker, sourceFile } = createChecker(
|
|
85
|
+
`const x = 'Enrich event ' + (event.id ?? event.name)`
|
|
86
|
+
)
|
|
87
|
+
const init = findInitializer(sourceFile)!
|
|
88
|
+
assert.equal(
|
|
89
|
+
extractStringLiteral(init, checker),
|
|
90
|
+
'Enrich event ${event.id ?? event.name}'
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('`+` concatenation and template literal produce the same display string', () => {
|
|
95
|
+
const concat = createChecker(
|
|
96
|
+
`const x = 'Enrich event ' + (event.id ?? event.name)`
|
|
97
|
+
)
|
|
98
|
+
const template = createChecker(
|
|
99
|
+
'const x = `Enrich event ${event.id ?? event.name}`'
|
|
100
|
+
)
|
|
101
|
+
const concatValue = extractStringLiteral(
|
|
102
|
+
findInitializer(concat.sourceFile)!,
|
|
103
|
+
concat.checker
|
|
104
|
+
)
|
|
105
|
+
const templateValue = extractStringLiteral(
|
|
106
|
+
findInitializer(template.sourceFile)!,
|
|
107
|
+
template.checker
|
|
108
|
+
)
|
|
109
|
+
assert.equal(concatValue, templateValue)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('still resolves fully-static concatenation exactly', () => {
|
|
113
|
+
const { checker, sourceFile } = createChecker(`const x = 'a' + 'b' + 'c'`)
|
|
114
|
+
const init = findInitializer(sourceFile)!
|
|
115
|
+
assert.equal(extractStringLiteral(init, checker), 'abc')
|
|
116
|
+
})
|
|
117
|
+
})
|