@pikku/inspector 0.12.33 → 0.12.35
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 +31 -0
- package/dist/add/add-http-route.js +4 -1
- package/dist/add/add-rpc-invocations.js +7 -0
- package/dist/add/add-workflow.js +35 -4
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/filter-inspector-state.js +43 -0
- package/dist/utils/load-addon-functions-meta.js +20 -14
- package/dist/utils/post-process.js +38 -13
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +9 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +12 -1
- package/dist/utils/workflow/dsl/patterns.js +37 -2
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +8 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -0
- package/package.json +2 -2
- package/run-tests.sh +4 -4
- package/src/add/add-http-route.ts +4 -1
- package/src/add/add-rpc-invocations.test.ts +113 -0
- package/src/add/add-rpc-invocations.ts +8 -0
- package/src/add/add-workflow.ts +42 -4
- package/src/inspector.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/filter-inspector-state.test.ts +231 -0
- package/src/utils/filter-inspector-state.ts +56 -0
- package/src/utils/load-addon-functions-meta.ts +26 -16
- package/src/utils/post-process.test.ts +254 -0
- package/src/utils/post-process.ts +41 -14
- package/src/utils/serialize-inspector-state.ts +11 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -3
- package/src/utils/workflow/dsl/patterns.ts +48 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +8 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.12.35
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 7ebea62: Tree-shake addon registrations in filtered inspector states (per-unit deploy codegen).
|
|
6
|
+
- `filterInspectorState` drops an addon's `wireAddonDeclarations`/`usedAddons` unless something kept actually references it (kept wiring targeting `namespace:*`, kept agent/MCP tool, or a body-level `rpc.invoke('namespace:*')` from a file that still contains a kept function). The generated per-unit bootstrap no longer imports unused addon package bootstraps — previously every deploy unit registered every addon's entire function surface, which pulled dev-only code (e.g. `@pikku/addon-console`'s static `node:fs` imports) into Cloudflare Worker bundles and failed upload with `No such module "node:fs"`.
|
|
7
|
+
- Body-level `rpc.invoke()` targets are now tracked per source file (`rpc.invokedFunctionsByFile`) so wiring-level `ref()` targets no longer pin an addon into every unit.
|
|
8
|
+
- `aggregateRequiredServices` computes addon parent services per used addon function (from the addon's shipped per-function `services` meta) instead of blanket-adding `addonRequiredParentServices` — and matches namespaced ids only, so bare project function names colliding with addon function names no longer force the blanket.
|
|
9
|
+
- Addon builds keep per-function `services` in the shipped `pikku-functions-meta.gen.json` so parent projects can do the above; addons built before this fall back to the blanket.
|
|
10
|
+
- HTTP route meta records `refTarget` for `ref('namespace:fn')`-wired routes, so per-unit filtering keeps the addon registration (and only that function's services) when the route deploys.
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [7ebea62]
|
|
13
|
+
- Updated dependencies [e57dd65]
|
|
14
|
+
- @pikku/core@0.12.51
|
|
15
|
+
|
|
16
|
+
## 0.12.34
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 92bd643: User flows in the console: workflow graph extraction now captures
|
|
21
|
+
`workflow.expectEventually` steps and per-step actor names (`{ actor:
|
|
22
|
+
actors.x }`), workflow meta carries `actors`/`title` into the serialized
|
|
23
|
+
graph, the CLI emits `user-flow-actors.gen.json` for the new
|
|
24
|
+
`MetaService.getUserFlowActorsMeta()`, and the console Workflows page gains a
|
|
25
|
+
Workflows / User Flows / Personas toggle. Also fixes complex-workflow graphs
|
|
26
|
+
being clobbered by a duplicate basic-extraction pass after successful DSL
|
|
27
|
+
extraction.
|
|
28
|
+
- Updated dependencies [35a9bab]
|
|
29
|
+
- Updated dependencies [92bd643]
|
|
30
|
+
- @pikku/core@0.12.50
|
|
31
|
+
|
|
1
32
|
## 0.12.33
|
|
2
33
|
|
|
3
34
|
### Patch Changes
|
|
@@ -220,8 +220,10 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
220
220
|
const middleware = resolveHTTPMiddlewareFromObject(state, fullRoute, obj, tags, checker);
|
|
221
221
|
// Resolve permissions
|
|
222
222
|
const permissions = resolveHTTPPermissionsFromObject(state, fullRoute, obj, tags, checker);
|
|
223
|
-
// Track used functions/middleware/permissions for service aggregation
|
|
224
223
|
state.serviceAggregation.usedFunctions.add(funcName);
|
|
224
|
+
if (refAddonTarget) {
|
|
225
|
+
state.serviceAggregation.usedFunctions.add(refAddonTarget);
|
|
226
|
+
}
|
|
225
227
|
extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
|
|
226
228
|
extractWireNames(permissions).forEach((name) => state.serviceAggregation.usedPermissions.add(name));
|
|
227
229
|
// Check for SSE
|
|
@@ -232,6 +234,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
232
234
|
state.http.files.add(sourceFile.fileName);
|
|
233
235
|
state.http.meta[method][fullRoute] = {
|
|
234
236
|
pikkuFuncId: funcName,
|
|
237
|
+
...(refAddonTarget && { refTarget: refAddonTarget }),
|
|
235
238
|
...(packageName && { packageName }),
|
|
236
239
|
route: fullRoute,
|
|
237
240
|
sourceFile: sourceFile.fileName,
|
|
@@ -85,10 +85,17 @@ export function addRPCInvocations(node, state, logger) {
|
|
|
85
85
|
const functionRef = firstArg.text;
|
|
86
86
|
logger.debug(`• Found RPC invocation: ${functionRef}`);
|
|
87
87
|
state.rpc.invokedFunctions.add(functionRef);
|
|
88
|
+
let byFile = state.rpc.invokedFunctionsByFile.get(sourceFileName);
|
|
89
|
+
if (!byFile) {
|
|
90
|
+
byFile = new Set();
|
|
91
|
+
state.rpc.invokedFunctionsByFile.set(sourceFileName, byFile);
|
|
92
|
+
}
|
|
93
|
+
byFile.add(functionRef);
|
|
88
94
|
const namespace = extractNamespace(functionRef);
|
|
89
95
|
if (namespace) {
|
|
90
96
|
logger.debug(` → Addon detected: ${namespace}`);
|
|
91
97
|
state.rpc.usedAddons.add(namespace);
|
|
98
|
+
state.serviceAggregation.usedFunctions.add(functionRef);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
// Handle template literals like `function-${name}`
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
5
5
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
6
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
7
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
-
import { getSourceText } from '../utils/workflow/dsl/patterns.js';
|
|
8
|
+
import { getSourceText, extractActorFromOptions, } from '../utils/workflow/dsl/patterns.js';
|
|
9
9
|
/**
|
|
10
10
|
* Extract a workflow step's display name without letting a non-static name
|
|
11
11
|
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
@@ -86,14 +86,18 @@ export function collectInvokedRPCs(steps, rpcs) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Scan for workflow.do(), workflow.
|
|
89
|
+
* Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
|
|
90
|
+
* workflow.cancel() calls to extract workflow steps
|
|
90
91
|
*/
|
|
91
92
|
function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
92
93
|
// Look for property access expressions: workflow.do or workflow.sleep
|
|
93
94
|
if (ts.isPropertyAccessExpression(node)) {
|
|
94
95
|
const { name } = node;
|
|
95
96
|
// Check if this is accessing 'do' or 'sleep' property
|
|
96
|
-
if (name.text === 'do' ||
|
|
97
|
+
if (name.text === 'do' ||
|
|
98
|
+
name.text === 'sleep' ||
|
|
99
|
+
name.text === 'cancel' ||
|
|
100
|
+
name.text === 'expectEventually') {
|
|
97
101
|
// Check if the parent is a call expression
|
|
98
102
|
const parent = node.parent;
|
|
99
103
|
if (ts.isCallExpression(parent) && parent.expression === node) {
|
|
@@ -113,6 +117,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
113
117
|
type: 'rpc',
|
|
114
118
|
stepName,
|
|
115
119
|
rpcName,
|
|
120
|
+
actor: extractActorFromOptions(optionsArg),
|
|
116
121
|
});
|
|
117
122
|
state.rpc.invokedFunctions.add(rpcName);
|
|
118
123
|
}
|
|
@@ -125,6 +130,21 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
}
|
|
133
|
+
else if (name.text === 'expectEventually' && args.length >= 4) {
|
|
134
|
+
// workflow.expectEventually(stepName, rpcName, data, predicate, options?)
|
|
135
|
+
const stepName = extractStepName(args[0], checker);
|
|
136
|
+
if (isStringLike(args[1], checker)) {
|
|
137
|
+
const rpcName = extractStringLiteral(args[1], checker);
|
|
138
|
+
steps.push({
|
|
139
|
+
type: 'rpc',
|
|
140
|
+
stepName,
|
|
141
|
+
rpcName,
|
|
142
|
+
expectEventually: true,
|
|
143
|
+
actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
144
|
+
});
|
|
145
|
+
state.rpc.invokedFunctions.add(rpcName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
else if (name.text === 'sleep' && args.length >= 2) {
|
|
129
149
|
// workflow.sleep(stepName, duration)
|
|
130
150
|
const stepNameArg = args[0];
|
|
@@ -282,9 +302,19 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
282
302
|
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
283
303
|
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
284
304
|
// with identifier args, etc.) are still registered as invoked functions.
|
|
305
|
+
// When DSL extraction already produced steps this pass is registration-only:
|
|
306
|
+
// appending to `steps` would duplicate nodes and clobber the graph.
|
|
285
307
|
if (wrapperType !== 'dsl') {
|
|
286
|
-
|
|
308
|
+
const sink = steps.length > 0 ? [] : steps;
|
|
309
|
+
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
|
|
287
310
|
}
|
|
311
|
+
// Actor names the flow's steps run as (user flows) — powers the console
|
|
312
|
+
// personas view and the User Flow graph badges.
|
|
313
|
+
const actorNames = [
|
|
314
|
+
...new Set(steps
|
|
315
|
+
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
316
|
+
.filter((a) => typeof a === 'string')),
|
|
317
|
+
];
|
|
288
318
|
state.workflows.meta[workflowName] = {
|
|
289
319
|
pikkuFuncId,
|
|
290
320
|
name: workflowName,
|
|
@@ -298,6 +328,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
298
328
|
tags,
|
|
299
329
|
expose,
|
|
300
330
|
userFlow: wrapperType === 'user-flow' ? true : undefined,
|
|
331
|
+
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
301
332
|
};
|
|
302
333
|
// User flows are pure stories of remote RPCs (same rule as client-side CLI
|
|
303
334
|
// renderers): the func may only destructure logger/config — everything else
|
package/dist/inspector.js
CHANGED
|
@@ -93,6 +93,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
93
93
|
exposedMeta: {},
|
|
94
94
|
exposedFiles: new Map(),
|
|
95
95
|
invokedFunctions: new Set(),
|
|
96
|
+
invokedFunctionsByFile: new Map(),
|
|
96
97
|
usedAddons: new Set(),
|
|
97
98
|
wireAddonDeclarations: new Map(),
|
|
98
99
|
wireAddonFiles: new Set(),
|
package/dist/types.d.ts
CHANGED
|
@@ -366,6 +366,9 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
366
366
|
// Track used functions/middleware/permissions
|
|
367
367
|
if (routeMeta.pikkuFuncId) {
|
|
368
368
|
filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncId);
|
|
369
|
+
if (routeMeta.refTarget) {
|
|
370
|
+
filteredState.serviceAggregation.usedFunctions.add(routeMeta.refTarget);
|
|
371
|
+
}
|
|
369
372
|
// For workflow/agent routes, also add the base name
|
|
370
373
|
// so the workflow/agent definition survives pruning
|
|
371
374
|
const colonIdx = routeMeta.pikkuFuncId.indexOf(':');
|
|
@@ -796,6 +799,46 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
796
799
|
if (filteredState.serviceAggregation.requiredServices.has('workflowService')) {
|
|
797
800
|
filteredState.serviceAggregation.requiredServices.add('queueService');
|
|
798
801
|
}
|
|
802
|
+
if ((filteredState.rpc.wireAddonDeclarations?.size ?? 0) > 0) {
|
|
803
|
+
const referencedIds = new Set(filteredState.serviceAggregation.usedFunctions);
|
|
804
|
+
const keptFiles = new Set();
|
|
805
|
+
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
806
|
+
const file = filteredState.functions.files.get(funcId);
|
|
807
|
+
if (file?.path)
|
|
808
|
+
keptFiles.add(file.path);
|
|
809
|
+
}
|
|
810
|
+
for (const [file, invoked] of state.rpc.invokedFunctionsByFile ??
|
|
811
|
+
new Map()) {
|
|
812
|
+
if (!keptFiles.has(file))
|
|
813
|
+
continue;
|
|
814
|
+
for (const id of invoked) {
|
|
815
|
+
referencedIds.add(id);
|
|
816
|
+
if (id.includes(':')) {
|
|
817
|
+
filteredState.serviceAggregation.usedFunctions.add(id);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const toolMeta of Object.values(filteredState.mcpEndpoints?.toolsMeta ?? {})) {
|
|
822
|
+
if (toolMeta.pikkuFuncId)
|
|
823
|
+
referencedIds.add(toolMeta.pikkuFuncId);
|
|
824
|
+
}
|
|
825
|
+
for (const agentMeta of Object.values(filteredState.agents?.agentsMeta ?? {})) {
|
|
826
|
+
for (const tool of agentMeta.tools ?? [])
|
|
827
|
+
referencedIds.add(tool);
|
|
828
|
+
}
|
|
829
|
+
const keptNamespaces = new Set();
|
|
830
|
+
for (const namespace of filteredState.rpc.wireAddonDeclarations.keys()) {
|
|
831
|
+
const prefix = `${namespace}:`;
|
|
832
|
+
for (const id of referencedIds) {
|
|
833
|
+
if (id.startsWith(prefix)) {
|
|
834
|
+
keptNamespaces.add(namespace);
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
filteredState.rpc.wireAddonDeclarations = new Map([...filteredState.rpc.wireAddonDeclarations].filter(([namespace]) => keptNamespaces.has(namespace)));
|
|
840
|
+
filteredState.rpc.usedAddons = new Set([...filteredState.rpc.usedAddons].filter((namespace) => keptNamespaces.has(namespace)));
|
|
841
|
+
}
|
|
799
842
|
// Recalculate requiredServices based on filtered functions/middleware/permissions
|
|
800
843
|
// Need to cast to InspectorState temporarily for aggregateRequiredServices
|
|
801
844
|
const stateForAggregation = filteredState;
|
|
@@ -135,6 +135,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
135
135
|
// No variables meta — that's fine
|
|
136
136
|
}
|
|
137
137
|
// Load addon serverlessIncompatible service names from pikku-addon-meta.gen.json
|
|
138
|
+
let loadedParentServices = false;
|
|
138
139
|
try {
|
|
139
140
|
const addonMetaPath = require.resolve(`${decl.package}/.pikku/console/pikku-addon-meta.gen.json`);
|
|
140
141
|
const addonMetaRaw = await readFile(addonMetaPath, 'utf-8');
|
|
@@ -144,24 +145,29 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
144
145
|
state.addonServerlessIncompatible.set(namespace, addonMeta.serverlessIncompatible);
|
|
145
146
|
logger.debug(`Addon '${namespace}' marks [${addonMeta.serverlessIncompatible.join(', ')}] as serverless-incompatible`);
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
// Load addon required parent services from pikku-services.gen
|
|
152
|
-
try {
|
|
153
|
-
const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
|
|
154
|
-
const servicesModule = await import(servicesGenPath);
|
|
155
|
-
if (servicesModule.requiredParentServices &&
|
|
156
|
-
Array.isArray(servicesModule.requiredParentServices)) {
|
|
157
|
-
for (const service of servicesModule.requiredParentServices) {
|
|
148
|
+
if (Array.isArray(addonMeta.requiredParentServices) &&
|
|
149
|
+
addonMeta.requiredParentServices.length > 0) {
|
|
150
|
+
for (const service of addonMeta.requiredParentServices) {
|
|
158
151
|
state.addonRequiredParentServices.push(service);
|
|
159
152
|
}
|
|
160
|
-
|
|
153
|
+
loadedParentServices = true;
|
|
154
|
+
logger.debug(`Loaded ${addonMeta.requiredParentServices.length} required parent services for '${namespace}' from addon meta`);
|
|
161
155
|
}
|
|
162
156
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
157
|
+
catch { }
|
|
158
|
+
if (!loadedParentServices) {
|
|
159
|
+
try {
|
|
160
|
+
const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
|
|
161
|
+
const servicesModule = await import(servicesGenPath);
|
|
162
|
+
if (servicesModule.requiredParentServices &&
|
|
163
|
+
Array.isArray(servicesModule.requiredParentServices)) {
|
|
164
|
+
for (const service of servicesModule.requiredParentServices) {
|
|
165
|
+
state.addonRequiredParentServices.push(service);
|
|
166
|
+
}
|
|
167
|
+
logger.debug(`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
165
171
|
}
|
|
166
172
|
try {
|
|
167
173
|
const httpContractsPath = require.resolve(`${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`);
|
|
@@ -253,19 +253,44 @@ export function aggregateRequiredServices(state) {
|
|
|
253
253
|
requiredServices.add('eventHub');
|
|
254
254
|
}
|
|
255
255
|
// 7. Services that consumed addons need from the parent project.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
const addonFnServices = new Map();
|
|
257
|
+
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
258
|
+
for (const [id, meta] of Object.entries(fns)) {
|
|
259
|
+
addonFnServices.set(`${namespace}:${id}`, meta?.services?.services);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const parentDeclared = state.addonRequiredParentServices ?? [];
|
|
263
|
+
const parentDeclaredSet = new Set(parentDeclared);
|
|
264
|
+
const defaultServices = new Set([
|
|
265
|
+
'config',
|
|
266
|
+
'logger',
|
|
267
|
+
'variables',
|
|
268
|
+
'schema',
|
|
269
|
+
'secrets',
|
|
270
|
+
]);
|
|
271
|
+
let usesAddonFn = false;
|
|
272
|
+
let addonFactoryNeeded = false;
|
|
273
|
+
for (const funcId of usedFunctions) {
|
|
274
|
+
if (!addonFnServices.has(funcId))
|
|
275
|
+
continue;
|
|
276
|
+
usesAddonFn = true;
|
|
277
|
+
const services = addonFnServices.get(funcId);
|
|
278
|
+
if (!services) {
|
|
279
|
+
addonFactoryNeeded = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const service of services) {
|
|
283
|
+
if (parentDeclaredSet.has(service)) {
|
|
284
|
+
requiredServices.add(service);
|
|
285
|
+
}
|
|
286
|
+
else if (!internalServices.has(service) &&
|
|
287
|
+
!defaultServices.has(service)) {
|
|
288
|
+
addonFactoryNeeded = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (usesAddonFn && addonFactoryNeeded) {
|
|
293
|
+
for (const service of parentDeclared) {
|
|
269
294
|
requiredServices.add(service);
|
|
270
295
|
}
|
|
271
296
|
}
|
|
@@ -78,6 +78,7 @@ export function serializeInspectorState(state) {
|
|
|
78
78
|
exposedMeta: state.rpc.exposedMeta,
|
|
79
79
|
exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
|
|
80
80
|
invokedFunctions: Array.from(state.rpc.invokedFunctions),
|
|
81
|
+
invokedFunctionsByFile: Array.from((state.rpc.invokedFunctionsByFile ?? new Map()).entries()).map(([file, fns]) => [file, Array.from(fns)]),
|
|
81
82
|
usedAddons: Array.from(state.rpc.usedAddons),
|
|
82
83
|
wireAddonDeclarations: Array.from(state.rpc.wireAddonDeclarations.entries()),
|
|
83
84
|
wireAddonFiles: Array.from(state.rpc.wireAddonFiles),
|
|
@@ -235,6 +236,7 @@ export function deserializeInspectorState(data) {
|
|
|
235
236
|
exposedMeta: data.rpc.exposedMeta,
|
|
236
237
|
exposedFiles: new Map(data.rpc.exposedFiles),
|
|
237
238
|
invokedFunctions: new Set(data.rpc.invokedFunctions),
|
|
239
|
+
invokedFunctionsByFile: new Map((data.rpc.invokedFunctionsByFile || []).map(([file, fns]) => [file, new Set(fns)])),
|
|
238
240
|
usedAddons: new Set(data.rpc.usedAddons || []),
|
|
239
241
|
wireAddonDeclarations: new Map(data.rpc.wireAddonDeclarations || []),
|
|
240
242
|
wireAddonFiles: new Set(data.rpc.wireAddonFiles || []),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
@@ -386,9 +386,12 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
386
386
|
const rpcName = extractStringLiteral(args[1], context.checker);
|
|
387
387
|
// Extract inputs from third argument
|
|
388
388
|
const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
|
|
389
|
+
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
390
|
+
const expectEventually = isWorkflowExpectEventuallyCall(call);
|
|
391
|
+
const optionsIndex = expectEventually ? 4 : 3;
|
|
392
|
+
const optionsArg = args.length > optionsIndex ? args[optionsIndex] : undefined;
|
|
393
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
394
|
+
? extractStepOptions(optionsArg, context)
|
|
392
395
|
: undefined;
|
|
393
396
|
return {
|
|
394
397
|
type: 'rpc',
|
|
@@ -397,6 +400,8 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
397
400
|
outputVar,
|
|
398
401
|
inputs,
|
|
399
402
|
options,
|
|
403
|
+
actor: extractActorFromOptions(optionsArg),
|
|
404
|
+
expectEventually: expectEventually || undefined,
|
|
400
405
|
};
|
|
401
406
|
}
|
|
402
407
|
catch (error) {
|
|
@@ -3,9 +3,20 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a call expression is workflow.expectEventually()
|
|
12
|
+
*/
|
|
13
|
+
export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extract the actor NAME from a step options object literal:
|
|
16
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
17
|
+
* or not statically resolvable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
|
|
9
20
|
/**
|
|
10
21
|
* Check if a call expression is workflow.sleep()
|
|
11
22
|
*/
|
|
@@ -3,17 +3,52 @@ import * as ts from 'typescript';
|
|
|
3
3
|
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Check if a call expression is workflow.do()
|
|
6
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
7
8
|
*/
|
|
8
9
|
export function isWorkflowDoCall(node, checker) {
|
|
9
10
|
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
const propAccess = node.expression;
|
|
13
|
-
return (propAccess.name.text === 'do'
|
|
14
|
+
return ((propAccess.name.text === 'do' ||
|
|
15
|
+
propAccess.name.text === 'expectEventually') &&
|
|
14
16
|
ts.isIdentifier(propAccess.expression) &&
|
|
15
17
|
propAccess.expression.text === 'workflow');
|
|
16
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if a call expression is workflow.expectEventually()
|
|
21
|
+
*/
|
|
22
|
+
export function isWorkflowExpectEventuallyCall(node) {
|
|
23
|
+
return (ts.isPropertyAccessExpression(node.expression) &&
|
|
24
|
+
node.expression.name.text === 'expectEventually' &&
|
|
25
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
26
|
+
node.expression.expression.text === 'workflow');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract the actor NAME from a step options object literal:
|
|
30
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
31
|
+
* or not statically resolvable.
|
|
32
|
+
*/
|
|
33
|
+
export function extractActorFromOptions(optionsArg) {
|
|
34
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
|
|
35
|
+
return;
|
|
36
|
+
for (const prop of optionsArg.properties) {
|
|
37
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
38
|
+
ts.isIdentifier(prop.name) &&
|
|
39
|
+
prop.name.text === 'actor') {
|
|
40
|
+
const init = prop.initializer;
|
|
41
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
42
|
+
return init.name.text;
|
|
43
|
+
}
|
|
44
|
+
if (ts.isElementAccessExpression(init) &&
|
|
45
|
+
ts.isStringLiteral(init.argumentExpression)) {
|
|
46
|
+
return init.argumentExpression.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
17
52
|
/**
|
|
18
53
|
* Check if a call expression is workflow.sleep()
|
|
19
54
|
*/
|
|
@@ -58,6 +58,12 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
58
58
|
rpcName: step.rpcName,
|
|
59
59
|
next: nextNodeId,
|
|
60
60
|
};
|
|
61
|
+
if (step.actor) {
|
|
62
|
+
node.actor = step.actor;
|
|
63
|
+
}
|
|
64
|
+
if (step.expectEventually) {
|
|
65
|
+
node.expectEventually = true;
|
|
66
|
+
}
|
|
61
67
|
if (step.inputs) {
|
|
62
68
|
if (step.inputs === 'passthrough') {
|
|
63
69
|
// Entire data is passed through - store as reference to trigger
|
|
@@ -304,8 +310,10 @@ export function convertDslToGraph(workflowName, meta) {
|
|
|
304
310
|
name: workflowName,
|
|
305
311
|
pikkuFuncId: meta.pikkuFuncId,
|
|
306
312
|
source,
|
|
313
|
+
title: meta.title,
|
|
307
314
|
description: meta.description,
|
|
308
315
|
tags: meta.tags,
|
|
316
|
+
actors: meta.actors,
|
|
309
317
|
context: meta.context,
|
|
310
318
|
nodes: nodesRecord,
|
|
311
319
|
entryNodeIds,
|
|
@@ -96,6 +96,10 @@ export interface FunctionNode extends BaseNode {
|
|
|
96
96
|
outputVar?: string;
|
|
97
97
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
98
98
|
stepHash?: string;
|
|
99
|
+
/** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
100
|
+
actor?: string;
|
|
101
|
+
/** True for workflow.expectEventually polling steps (user flows) */
|
|
102
|
+
expectEventually?: boolean;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Flow node - control flow only, no RPC call
|
|
@@ -136,10 +140,14 @@ export interface SerializedWorkflowGraph {
|
|
|
136
140
|
pikkuFuncId: string;
|
|
137
141
|
/** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
|
|
138
142
|
source: WorkflowSourceType;
|
|
143
|
+
/** Optional short display name */
|
|
144
|
+
title?: string;
|
|
139
145
|
/** Optional description */
|
|
140
146
|
description?: string;
|
|
141
147
|
/** Tags for organization */
|
|
142
148
|
tags?: string[];
|
|
149
|
+
/** Actor names a user flow's steps run as */
|
|
150
|
+
actors?: string[];
|
|
143
151
|
/** If true, workflow always executes inline without queues */
|
|
144
152
|
inline?: boolean;
|
|
145
153
|
/** Workflow context/state variables (from Zod schema) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.35",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.51",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "2.9.1-next.16",
|
package/run-tests.sh
CHANGED
|
@@ -38,16 +38,16 @@ if [ ${#files[@]} -eq 0 ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
# Construct the node command
|
|
41
|
-
node_cmd=
|
|
41
|
+
node_cmd=(node --import tsx --test)
|
|
42
42
|
|
|
43
43
|
# Append options based on flags
|
|
44
44
|
if [ "$watch_mode" = true ]; then
|
|
45
|
-
node_cmd
|
|
45
|
+
node_cmd+=(--watch)
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
if [ "$coverage_mode" = true ]; then
|
|
49
|
-
node_cmd
|
|
49
|
+
node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# Execute the node command with the expanded list of files
|
|
53
|
-
$node_cmd "${files[@]}"
|
|
53
|
+
"${node_cmd[@]}" "${files[@]}"
|
|
@@ -389,8 +389,10 @@ export function registerHTTPRoute({
|
|
|
389
389
|
checker
|
|
390
390
|
)
|
|
391
391
|
|
|
392
|
-
// Track used functions/middleware/permissions for service aggregation
|
|
393
392
|
state.serviceAggregation.usedFunctions.add(funcName)
|
|
393
|
+
if (refAddonTarget) {
|
|
394
|
+
state.serviceAggregation.usedFunctions.add(refAddonTarget)
|
|
395
|
+
}
|
|
394
396
|
extractWireNames(middleware).forEach((name) =>
|
|
395
397
|
state.serviceAggregation.usedMiddleware.add(name)
|
|
396
398
|
)
|
|
@@ -415,6 +417,7 @@ export function registerHTTPRoute({
|
|
|
415
417
|
state.http.files.add(sourceFile.fileName)
|
|
416
418
|
state.http.meta[method][fullRoute] = {
|
|
417
419
|
pikkuFuncId: funcName,
|
|
420
|
+
...(refAddonTarget && { refTarget: refAddonTarget }),
|
|
418
421
|
...(packageName && { packageName }),
|
|
419
422
|
route: fullRoute,
|
|
420
423
|
sourceFile: sourceFile.fileName,
|