@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
|
@@ -1,45 +1,63 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
AddWiring,
|
|
5
|
+
ExportedHTTPRouteConfigMeta,
|
|
6
|
+
ExportedHTTPRouteMapMeta,
|
|
7
|
+
ExportedHTTPRoutesGroupMeta,
|
|
8
|
+
InspectorLogger,
|
|
9
|
+
InspectorState,
|
|
10
|
+
} from '../types.js'
|
|
11
|
+
import { registerHTTPRoute, registerHTTPRouteMeta } from './add-http-route.js'
|
|
5
12
|
import { resolveIdentifier } from '../utils/resolve-identifier.js'
|
|
13
|
+
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
14
|
+
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
|
|
15
|
+
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
16
|
+
import {
|
|
17
|
+
resolveRefContract,
|
|
18
|
+
type RefContractResolution,
|
|
19
|
+
} from '../utils/resolve-ref-contract.js'
|
|
20
|
+
import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
|
|
6
21
|
|
|
7
|
-
/**
|
|
8
|
-
* Group configuration extracted from wireHTTPRoutes or defineHTTPRoutes
|
|
9
|
-
*/
|
|
10
22
|
interface GroupConfig {
|
|
11
23
|
basePath: string
|
|
12
24
|
tags: string[]
|
|
13
25
|
auth?: boolean
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
/**
|
|
17
|
-
* Process wireHTTPRoutes calls
|
|
18
|
-
*/
|
|
19
28
|
export const addHTTPRoutes: AddWiring = (
|
|
20
29
|
logger,
|
|
21
30
|
node,
|
|
22
31
|
checker,
|
|
23
32
|
state,
|
|
24
|
-
|
|
33
|
+
options
|
|
25
34
|
) => {
|
|
26
35
|
if (!ts.isCallExpression(node)) return
|
|
27
36
|
|
|
28
37
|
const { expression, arguments: args } = node
|
|
29
|
-
if (!ts.isIdentifier(expression)
|
|
38
|
+
if (!ts.isIdentifier(expression)) return
|
|
39
|
+
|
|
40
|
+
if (expression.text === 'defineHTTPRoutes') {
|
|
41
|
+
const exportName = getExportedVariableName(node, options.sourceFile)
|
|
42
|
+
const firstArg = args[0]
|
|
43
|
+
if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
44
|
+
const contract = serializeHTTPRoutesContract(firstArg, checker, state)
|
|
45
|
+
if (contract) {
|
|
46
|
+
state.exportedContracts.http[exportName] = contract
|
|
47
|
+
}
|
|
48
|
+
}
|
|
30
49
|
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (expression.text !== 'wireHTTPRoutes') return
|
|
31
53
|
|
|
32
54
|
const firstArg = args[0]
|
|
33
55
|
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
|
|
34
56
|
|
|
35
|
-
// Extract group config
|
|
36
57
|
const groupConfig = extractGroupConfig(firstArg)
|
|
37
|
-
|
|
38
|
-
// Get routes property
|
|
39
58
|
const routesProp = getPropertyAssignment(firstArg, 'routes')
|
|
40
59
|
if (!routesProp) return
|
|
41
60
|
|
|
42
|
-
// Process routes recursively
|
|
43
61
|
processRoutes(
|
|
44
62
|
routesProp.initializer,
|
|
45
63
|
groupConfig,
|
|
@@ -50,9 +68,6 @@ export const addHTTPRoutes: AddWiring = (
|
|
|
50
68
|
)
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
/**
|
|
54
|
-
* Get a property assignment from an object literal
|
|
55
|
-
*/
|
|
56
71
|
function getPropertyAssignment(
|
|
57
72
|
obj: ts.ObjectLiteralExpression,
|
|
58
73
|
propName: string
|
|
@@ -69,9 +84,6 @@ function getPropertyAssignment(
|
|
|
69
84
|
return undefined
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
/**
|
|
73
|
-
* Extract group configuration from an object literal
|
|
74
|
-
*/
|
|
75
87
|
function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
|
|
76
88
|
const basePath = (getPropertyValue(obj, 'basePath') as string) || ''
|
|
77
89
|
const tags = (getPropertyValue(obj, 'tags') as string[]) || []
|
|
@@ -84,9 +96,6 @@ function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
/**
|
|
88
|
-
* Merge two group configs following cascading rules
|
|
89
|
-
*/
|
|
90
99
|
function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
|
|
91
100
|
return {
|
|
92
101
|
basePath: parent.basePath + child.basePath,
|
|
@@ -95,9 +104,6 @@ function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
|
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
/**
|
|
99
|
-
* Check if a value is a route config (has method, func, and route)
|
|
100
|
-
*/
|
|
101
107
|
function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
|
|
102
108
|
let hasMethod = false
|
|
103
109
|
let hasFunc = false
|
|
@@ -114,9 +120,6 @@ function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
|
|
|
114
120
|
return hasMethod && hasFunc && hasRoute
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
/**
|
|
118
|
-
* Check if a value is a route contract (has routes property but no method/func)
|
|
119
|
-
*/
|
|
120
123
|
function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
|
|
121
124
|
let hasRoutes = false
|
|
122
125
|
let hasMethod = false
|
|
@@ -133,9 +136,6 @@ function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
|
|
|
133
136
|
return hasRoutes && !hasMethod && !hasFunc
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
/**
|
|
137
|
-
* Recursively process routes - handles nested maps, contracts, and identifiers
|
|
138
|
-
*/
|
|
139
139
|
function processRoutes(
|
|
140
140
|
node: ts.Node,
|
|
141
141
|
parentConfig: GroupConfig,
|
|
@@ -144,7 +144,6 @@ function processRoutes(
|
|
|
144
144
|
logger: InspectorLogger,
|
|
145
145
|
sourceFile: ts.SourceFile
|
|
146
146
|
): void {
|
|
147
|
-
// Handle array of routes
|
|
148
147
|
if (ts.isArrayLiteralExpression(node)) {
|
|
149
148
|
for (const element of node.elements) {
|
|
150
149
|
if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
|
|
@@ -154,15 +153,12 @@ function processRoutes(
|
|
|
154
153
|
return
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
// Handle object literal
|
|
158
156
|
if (ts.isObjectLiteralExpression(node)) {
|
|
159
|
-
// Check if this is a route config
|
|
160
157
|
if (isRouteConfig(node)) {
|
|
161
158
|
processRoute(node, parentConfig, state, checker, logger, sourceFile)
|
|
162
159
|
return
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
// Check if this is a route contract
|
|
166
162
|
if (isRouteContract(node)) {
|
|
167
163
|
const contractConfig = extractGroupConfig(node)
|
|
168
164
|
const mergedConfig = mergeConfigs(parentConfig, contractConfig)
|
|
@@ -180,9 +176,17 @@ function processRoutes(
|
|
|
180
176
|
return
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
// Otherwise it's a nested map - process each property
|
|
184
179
|
for (const prop of node.properties) {
|
|
185
180
|
if (ts.isPropertyAssignment(prop)) {
|
|
181
|
+
const ref = resolveRefContract(
|
|
182
|
+
prop.initializer,
|
|
183
|
+
'refHTTP',
|
|
184
|
+
state.exportedContracts.addonHttp
|
|
185
|
+
)
|
|
186
|
+
if (ref) {
|
|
187
|
+
processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
186
190
|
processRoutes(
|
|
187
191
|
prop.initializer,
|
|
188
192
|
parentConfig,
|
|
@@ -196,7 +200,18 @@ function processRoutes(
|
|
|
196
200
|
return
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
|
|
203
|
+
if (ts.isCallExpression(node)) {
|
|
204
|
+
const ref = resolveRefContract(
|
|
205
|
+
node,
|
|
206
|
+
'refHTTP',
|
|
207
|
+
state.exportedContracts.addonHttp
|
|
208
|
+
)
|
|
209
|
+
if (ref) {
|
|
210
|
+
processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
|
|
211
|
+
}
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
if (ts.isIdentifier(node)) {
|
|
201
216
|
const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes'])
|
|
202
217
|
if (resolved) {
|
|
@@ -205,9 +220,236 @@ function processRoutes(
|
|
|
205
220
|
}
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
function processRefHTTPContract(
|
|
224
|
+
ref: RefContractResolution<ExportedHTTPRoutesGroupMeta>,
|
|
225
|
+
parentConfig: GroupConfig,
|
|
226
|
+
state: InspectorState,
|
|
227
|
+
logger: InspectorLogger,
|
|
228
|
+
sourceFile: ts.SourceFile
|
|
229
|
+
): void {
|
|
230
|
+
const basePath =
|
|
231
|
+
ref.basePath !== undefined ? ref.basePath : ref.contract.basePath || ''
|
|
232
|
+
processExportedRouteMap(
|
|
233
|
+
ref.contract.routes,
|
|
234
|
+
mergeConfigs(parentConfig, {
|
|
235
|
+
basePath,
|
|
236
|
+
tags: ref.contract.tags || [],
|
|
237
|
+
auth: ref.contract.auth,
|
|
238
|
+
}),
|
|
239
|
+
state,
|
|
240
|
+
logger,
|
|
241
|
+
sourceFile
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function processExportedRouteMap(
|
|
246
|
+
routes: ExportedHTTPRouteMapMeta,
|
|
247
|
+
parentConfig: GroupConfig,
|
|
248
|
+
state: InspectorState,
|
|
249
|
+
logger: InspectorLogger,
|
|
250
|
+
sourceFile: ts.SourceFile
|
|
251
|
+
): void {
|
|
252
|
+
for (const value of Object.values(routes)) {
|
|
253
|
+
if (isExportedRouteConfig(value)) {
|
|
254
|
+
registerHTTPRouteMeta({
|
|
255
|
+
route: value,
|
|
256
|
+
state,
|
|
257
|
+
logger,
|
|
258
|
+
sourceFile,
|
|
259
|
+
basePath: parentConfig.basePath,
|
|
260
|
+
inheritedTags: parentConfig.tags,
|
|
261
|
+
inheritedAuth: parentConfig.auth,
|
|
262
|
+
})
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (isExportedRouteContract(value)) {
|
|
267
|
+
processExportedRouteMap(
|
|
268
|
+
value.routes,
|
|
269
|
+
mergeConfigs(parentConfig, {
|
|
270
|
+
basePath: value.basePath || '',
|
|
271
|
+
tags: value.tags || [],
|
|
272
|
+
auth: value.auth,
|
|
273
|
+
}),
|
|
274
|
+
state,
|
|
275
|
+
logger,
|
|
276
|
+
sourceFile
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
processExportedRouteMap(value, parentConfig, state, logger, sourceFile)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isExportedRouteConfig(
|
|
286
|
+
value: ExportedHTTPRouteMapMeta[string]
|
|
287
|
+
): value is ExportedHTTPRouteConfigMeta {
|
|
288
|
+
return (
|
|
289
|
+
typeof value === 'object' &&
|
|
290
|
+
value !== null &&
|
|
291
|
+
'method' in value &&
|
|
292
|
+
'route' in value &&
|
|
293
|
+
'func' in value
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isExportedRouteContract(
|
|
298
|
+
value: ExportedHTTPRouteMapMeta[string]
|
|
299
|
+
): value is ExportedHTTPRoutesGroupMeta {
|
|
300
|
+
return (
|
|
301
|
+
typeof value === 'object' &&
|
|
302
|
+
value !== null &&
|
|
303
|
+
'routes' in value &&
|
|
304
|
+
!('method' in value)
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function serializeHTTPRoutesContract(
|
|
309
|
+
node: ts.ObjectLiteralExpression,
|
|
310
|
+
checker: ts.TypeChecker,
|
|
311
|
+
state: InspectorState
|
|
312
|
+
): ExportedHTTPRoutesGroupMeta | null {
|
|
313
|
+
if (isRouteContract(node)) {
|
|
314
|
+
const routesProp = getPropertyAssignment(node, 'routes')
|
|
315
|
+
if (!routesProp || !ts.isObjectLiteralExpression(routesProp.initializer)) {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
...extractGroupConfig(node),
|
|
321
|
+
routes: serializeHTTPRouteMap(routesProp.initializer, checker, state),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
routes: serializeHTTPRouteMap(node, checker, state),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function serializeHTTPRouteMap(
|
|
331
|
+
node: ts.ObjectLiteralExpression,
|
|
332
|
+
checker: ts.TypeChecker,
|
|
333
|
+
state: InspectorState
|
|
334
|
+
): ExportedHTTPRouteMapMeta {
|
|
335
|
+
const result: ExportedHTTPRouteMapMeta = {}
|
|
336
|
+
|
|
337
|
+
for (const prop of node.properties) {
|
|
338
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
339
|
+
|
|
340
|
+
const key = prop.name.getText().replace(/^['"]|['"]$/g, '')
|
|
341
|
+
const value = prop.initializer
|
|
342
|
+
|
|
343
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
344
|
+
if (isRouteConfig(value)) {
|
|
345
|
+
const route = serializeHTTPRouteConfig(value, checker, state)
|
|
346
|
+
if (route) {
|
|
347
|
+
result[key] = route
|
|
348
|
+
}
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isRouteContract(value)) {
|
|
353
|
+
const routeContract = serializeHTTPRoutesContract(value, checker, state)
|
|
354
|
+
if (routeContract) {
|
|
355
|
+
result[key] = routeContract
|
|
356
|
+
}
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
result[key] = serializeHTTPRouteMap(value, checker, state)
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (ts.isIdentifier(value)) {
|
|
365
|
+
const resolved = resolveIdentifier(value, checker, ['defineHTTPRoutes'])
|
|
366
|
+
if (resolved && ts.isObjectLiteralExpression(resolved)) {
|
|
367
|
+
if (isRouteContract(resolved)) {
|
|
368
|
+
const routeContract = serializeHTTPRoutesContract(
|
|
369
|
+
resolved,
|
|
370
|
+
checker,
|
|
371
|
+
state
|
|
372
|
+
)
|
|
373
|
+
if (routeContract) {
|
|
374
|
+
result[key] = routeContract
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
result[key] = serializeHTTPRouteMap(resolved, checker, state)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function serializeHTTPRouteConfig(
|
|
387
|
+
obj: ts.ObjectLiteralExpression,
|
|
388
|
+
checker: ts.TypeChecker,
|
|
389
|
+
state: InspectorState
|
|
390
|
+
): ExportedHTTPRouteConfigMeta | null {
|
|
391
|
+
const method = getPropertyValue(obj, 'method') as string | null
|
|
392
|
+
const route = getPropertyValue(obj, 'route') as string | null
|
|
393
|
+
const funcInitializer = getPropertyAssignmentInitializer(
|
|
394
|
+
obj,
|
|
395
|
+
'func',
|
|
396
|
+
true,
|
|
397
|
+
checker
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if (!method || !route || !funcInitializer) {
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let pikkuFuncId = extractFunctionName(
|
|
405
|
+
funcInitializer,
|
|
406
|
+
checker,
|
|
407
|
+
state.rootDir
|
|
408
|
+
).pikkuFuncId
|
|
409
|
+
let packageName: string | undefined
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
ts.isCallExpression(funcInitializer) &&
|
|
413
|
+
ts.isIdentifier(funcInitializer.expression) &&
|
|
414
|
+
funcInitializer.expression.text === 'ref'
|
|
415
|
+
) {
|
|
416
|
+
const [firstArg] = funcInitializer.arguments
|
|
417
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
418
|
+
pikkuFuncId = firstArg.text
|
|
419
|
+
const addonNamespace = pikkuFuncId.includes(':')
|
|
420
|
+
? pikkuFuncId.split(':')[0]
|
|
421
|
+
: null
|
|
422
|
+
packageName = addonNamespace
|
|
423
|
+
? state.rpc.wireAddonDeclarations.get(addonNamespace)?.package
|
|
424
|
+
: undefined
|
|
425
|
+
}
|
|
426
|
+
} else if (ts.isIdentifier(funcInitializer)) {
|
|
427
|
+
packageName =
|
|
428
|
+
resolveAddonName(
|
|
429
|
+
funcInitializer,
|
|
430
|
+
checker,
|
|
431
|
+
state.rpc.wireAddonDeclarations
|
|
432
|
+
) || undefined
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
auth: getPropertyValue(obj, 'auth') as boolean | undefined,
|
|
437
|
+
contentType: getPropertyValue(obj, 'contentType') as string | undefined,
|
|
438
|
+
headers:
|
|
439
|
+
(getPropertyValue(obj, 'headers') as unknown as Record<string, string>) ||
|
|
440
|
+
undefined,
|
|
441
|
+
method,
|
|
442
|
+
route,
|
|
443
|
+
sse: getPropertyValue(obj, 'sse') as boolean | undefined,
|
|
444
|
+
tags: (getPropertyValue(obj, 'tags') as string[]) || undefined,
|
|
445
|
+
timeout: getPropertyValue(obj, 'timeout') as number | undefined,
|
|
446
|
+
func: {
|
|
447
|
+
pikkuFuncId,
|
|
448
|
+
...(packageName && { packageName }),
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
211
453
|
function processRoute(
|
|
212
454
|
obj: ts.ObjectLiteralExpression,
|
|
213
455
|
groupConfig: GroupConfig,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { strict as assert } from '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 type { InspectorLogger } from '../types.js'
|
|
8
|
+
|
|
9
|
+
function makeLogger(
|
|
10
|
+
criticals: Array<{ code: string; message: string }>
|
|
11
|
+
): InspectorLogger {
|
|
12
|
+
return {
|
|
13
|
+
debug: () => {},
|
|
14
|
+
info: () => {},
|
|
15
|
+
warn: () => {},
|
|
16
|
+
error: () => {},
|
|
17
|
+
diagnostic: ({ code, message }) => {
|
|
18
|
+
criticals.push({ code, message })
|
|
19
|
+
},
|
|
20
|
+
critical: (code: any, message: string) => {
|
|
21
|
+
criticals.push({ code, message })
|
|
22
|
+
},
|
|
23
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const STEP_FILE = [
|
|
28
|
+
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
29
|
+
'export const processEventLeadsStep = pikkuSessionlessFunc({',
|
|
30
|
+
' func: async ({ logger }) => ({ persistedCount: 1 }),',
|
|
31
|
+
'})',
|
|
32
|
+
].join('\n')
|
|
33
|
+
|
|
34
|
+
describe('addWorkflow — Promise.all fanout RPC detection', () => {
|
|
35
|
+
test('registers fanout RPC when captured with `const x = await Promise.all(map(...))`', async () => {
|
|
36
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-fanout-const-'))
|
|
37
|
+
const wfFile = join(rootDir, 'leads.workflow.ts')
|
|
38
|
+
const stepFile = join(rootDir, 'leads.steps.ts')
|
|
39
|
+
|
|
40
|
+
await writeFile(stepFile, STEP_FILE)
|
|
41
|
+
await writeFile(
|
|
42
|
+
wfFile,
|
|
43
|
+
[
|
|
44
|
+
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
45
|
+
'export const extractLeadsWorkflow = pikkuWorkflowFunc(async (_, data, { workflow }) => {',
|
|
46
|
+
' const events = [{ id: "a", name: "x" }]',
|
|
47
|
+
' const processed = await Promise.all(',
|
|
48
|
+
' events.map((event) =>',
|
|
49
|
+
" workflow.do(`Enrich event ${event.id ?? event.name}`, 'processEventLeadsStep', { event })",
|
|
50
|
+
' )',
|
|
51
|
+
' )',
|
|
52
|
+
' return { count: processed.length }',
|
|
53
|
+
'})',
|
|
54
|
+
].join('\n')
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const criticals: Array<{ code: string; message: string }> = []
|
|
58
|
+
try {
|
|
59
|
+
const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
|
|
60
|
+
rootDir,
|
|
61
|
+
})
|
|
62
|
+
assert.ok(
|
|
63
|
+
state.rpc.invokedFunctions.has('processEventLeadsStep'),
|
|
64
|
+
'processEventLeadsStep should be registered when fanout is captured with const'
|
|
65
|
+
)
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('registers fanout RPC with string-concatenation (`+`) step name, same as template literal', async () => {
|
|
72
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-fanout-concat-'))
|
|
73
|
+
const wfFile = join(rootDir, 'leads.workflow.ts')
|
|
74
|
+
const stepFile = join(rootDir, 'leads.steps.ts')
|
|
75
|
+
|
|
76
|
+
await writeFile(stepFile, STEP_FILE)
|
|
77
|
+
await writeFile(
|
|
78
|
+
wfFile,
|
|
79
|
+
[
|
|
80
|
+
"import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
|
|
81
|
+
'export const extractLeadsWorkflow = pikkuWorkflowFunc(async (_, data, { workflow }) => {',
|
|
82
|
+
' const events = [{ id: "a", name: "x" }]',
|
|
83
|
+
' await Promise.all(',
|
|
84
|
+
' events.map((event) =>',
|
|
85
|
+
" workflow.do('Enrich event ' + (event.id ?? event.name), 'processEventLeadsStep', { event })",
|
|
86
|
+
' )',
|
|
87
|
+
' )',
|
|
88
|
+
' return { ok: true }',
|
|
89
|
+
'})',
|
|
90
|
+
].join('\n')
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const criticals: Array<{ code: string; message: string }> = []
|
|
94
|
+
try {
|
|
95
|
+
const state = await inspect(makeLogger(criticals), [stepFile, wfFile], {
|
|
96
|
+
rootDir,
|
|
97
|
+
})
|
|
98
|
+
assert.ok(
|
|
99
|
+
state.rpc.invokedFunctions.has('processEventLeadsStep'),
|
|
100
|
+
'processEventLeadsStep should be registered even when the step name uses `+` concatenation with a non-static operand'
|
|
101
|
+
)
|
|
102
|
+
} finally {
|
|
103
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -16,6 +16,20 @@ import {
|
|
|
16
16
|
getPropertyValue,
|
|
17
17
|
} from '../utils/get-property-value.js'
|
|
18
18
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
|
|
19
|
+
import { getSourceText } from '../utils/workflow/dsl/patterns.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract a workflow step's display name without letting a non-static name
|
|
23
|
+
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
24
|
+
* resolution failure must never prevent the RPC from being registered.
|
|
25
|
+
*/
|
|
26
|
+
function extractStepName(node: ts.Node, checker: ts.TypeChecker): string {
|
|
27
|
+
try {
|
|
28
|
+
return extractStringLiteral(node, checker)
|
|
29
|
+
} catch {
|
|
30
|
+
return getSourceText(node)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
/**
|
|
21
35
|
* Recursively check if any step has inline type (non-serializable)
|
|
@@ -99,7 +113,7 @@ function getWorkflowInvocations(
|
|
|
99
113
|
const optionsArg =
|
|
100
114
|
args.length >= 4 ? args[args.length - 1] : undefined
|
|
101
115
|
|
|
102
|
-
const stepName =
|
|
116
|
+
const stepName = extractStepName(stepNameArg, checker)
|
|
103
117
|
const description =
|
|
104
118
|
extractDescription(optionsArg, checker) ?? undefined
|
|
105
119
|
|
|
@@ -126,7 +140,7 @@ function getWorkflowInvocations(
|
|
|
126
140
|
const stepNameArg = args[0]
|
|
127
141
|
const durationArg = args[1]
|
|
128
142
|
|
|
129
|
-
const stepName =
|
|
143
|
+
const stepName = extractStepName(stepNameArg, checker)
|
|
130
144
|
const duration = extractDuration(durationArg, checker)
|
|
131
145
|
|
|
132
146
|
steps.push({
|