@pikku/inspector 0.12.27 → 0.12.29
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 +60 -0
- package/dist/add/add-file-with-factory.js +1 -0
- package/dist/add/add-functions.js +9 -3
- package/dist/add/add-rpc-invocations.js +27 -0
- package/dist/error-codes.d.ts +2 -1
- package/dist/error-codes.js +1 -0
- package/dist/inspector.js +2 -0
- package/dist/types.d.ts +9 -0
- package/dist/utils/filter-inspector-state.js +7 -6
- package/dist/utils/get-files-and-methods.d.ts +2 -1
- package/dist/utils/get-files-and-methods.js +2 -1
- package/dist/utils/load-addon-functions-meta.js +14 -0
- package/dist/utils/post-process.js +8 -1
- package/dist/utils/resolve-deploy-target.d.ts +3 -2
- package/dist/utils/resolve-deploy-target.js +4 -3
- package/dist/utils/serialize-inspector-state.d.ts +9 -0
- package/dist/utils/serialize-inspector-state.js +4 -0
- package/dist/utils/workflow/derive-workflow-plan.js +19 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +26 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
- package/dist/utils/workflow/dsl/patterns.js +12 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +0 -2
- package/dist/visit.js +1 -0
- package/package.json +2 -2
- package/src/add/add-file-with-factory.ts +1 -0
- package/src/add/add-functions.ts +9 -3
- package/src/add/add-rpc-invocations.ts +41 -0
- package/src/add/pii-check.test.ts +5 -2
- package/src/add/rpc-type-cast.test.ts +123 -0
- package/src/error-codes.ts +2 -0
- package/src/inspector.ts +2 -0
- package/src/types.ts +12 -0
- package/src/utils/filter-inspector-state.test.ts +110 -1
- package/src/utils/filter-inspector-state.ts +13 -7
- package/src/utils/get-files-and-methods.ts +6 -0
- package/src/utils/load-addon-functions-meta.ts +23 -0
- package/src/utils/post-process.ts +8 -1
- package/src/utils/resolve-deploy-target.test.ts +30 -0
- package/src/utils/resolve-deploy-target.ts +5 -3
- package/src/utils/serialize-inspector-state.ts +17 -0
- package/src/utils/workflow/derive-workflow-plan.test.ts +18 -0
- package/src/utils/workflow/derive-workflow-plan.ts +20 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +31 -0
- package/src/utils/workflow/dsl/patterns.ts +19 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -2
- package/src/visit.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
## 0.12.29
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 7b5b10a: fix(workflow): include suspend steps in plannedSteps with readable displayName
|
|
6
|
+
|
|
7
|
+
`workflow.suspend(reason)` calls now appear in the static `plannedSteps` ladder
|
|
8
|
+
produced by `deriveWorkflowPlan`. Previously the inspector ignored them, so the
|
|
9
|
+
runtime's `__workflow_suspend:<reason>` steps had no planned counterpart and
|
|
10
|
+
the UI appended them as orphans at the bottom of the step list instead of
|
|
11
|
+
showing them at the correct position.
|
|
12
|
+
|
|
13
|
+
Changes:
|
|
14
|
+
- `WorkflowPlannedStep` gains an optional `displayName` field — the human-
|
|
15
|
+
readable label to show in the UI (falls back to `stepName` when absent).
|
|
16
|
+
- New `SuspendStepMeta` type added to `WorkflowStepMeta`.
|
|
17
|
+
- Inspector extracts `workflow.suspend('reason')` calls and emits a
|
|
18
|
+
`SuspendStepMeta` step with `type: 'suspend'` and `reason`.
|
|
19
|
+
- `collectNamedSteps` maps a suspend step to
|
|
20
|
+
`{ stepName: '__workflow_suspend:<reason>', displayName: '<reason>' }`,
|
|
21
|
+
matching the key the runtime stores so the UI can overlay live status
|
|
22
|
+
onto the planned position.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [7b5b10a]
|
|
25
|
+
- @pikku/core@0.12.42
|
|
26
|
+
|
|
27
|
+
## 0.12.28
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 66d43d1: Add `deploy.defaultTarget` to `pikku.config.json` to override the default deploy target ('serverless') for functions without an explicit `deploy` flag.
|
|
32
|
+
- a8c9e6d: feat(inspector): add PKU940 — block type casts on rpc.invoke() calls
|
|
33
|
+
|
|
34
|
+
The inspector now emits a critical PKU940 error when `rpc.invoke()` is called
|
|
35
|
+
with an `as` cast on an argument (`rpc.invoke('fn', data as any)`) or when its
|
|
36
|
+
result is cast (`rpc.invoke('fn', data) as any`). Both patterns defeat Pikku's
|
|
37
|
+
generated type safety and are rejected at build time.
|
|
38
|
+
|
|
39
|
+
- ba1ab08: refactor(workflow): replace `inline: false` with `workflowQueued: true` on function meta
|
|
40
|
+
|
|
41
|
+
The per-function workflow dispatch flag has been renamed from the confusing
|
|
42
|
+
negative `inline: false` to the explicit positive `workflowQueued: true`.
|
|
43
|
+
Two companion fields are also added: `workflowRetries` and `workflowTimeout`
|
|
44
|
+
as function-level equivalents of the per-call-site `NodeOptions` fields.
|
|
45
|
+
|
|
46
|
+
**Breaking change (patch — flag was undocumented):** rename `inline: false`
|
|
47
|
+
to `workflowQueued: true` on any `pikkuSessionlessFunc` / `pikkuFunc` that
|
|
48
|
+
dispatches its workflow steps via the queue.
|
|
49
|
+
|
|
50
|
+
**Behaviour change:** a step marked `workflowQueued: true` now throws if no
|
|
51
|
+
queue service is configured, instead of silently falling back to inline
|
|
52
|
+
execution.
|
|
53
|
+
|
|
54
|
+
**Bug fix:** `post-process.ts` was registering `wf-step-*` queues for every
|
|
55
|
+
workflow step node; it now only registers them for steps that are actually
|
|
56
|
+
`workflowQueued: true`, avoiding spurious queue resource allocation.
|
|
57
|
+
|
|
58
|
+
- Updated dependencies [ba1ab08]
|
|
59
|
+
- @pikku/core@0.12.40
|
|
60
|
+
|
|
1
61
|
## 0.12.27
|
|
2
62
|
|
|
3
63
|
### Patch Changes
|
|
@@ -8,6 +8,7 @@ const wrapperFunctionMap = {
|
|
|
8
8
|
pikkuAddonServices: 'CreateSingletonServices',
|
|
9
9
|
pikkuWireServices: 'CreateWireServices',
|
|
10
10
|
pikkuAddonWireServices: 'CreateWireServices',
|
|
11
|
+
pikkuServerLifecycle: 'ServerLifecycle',
|
|
11
12
|
};
|
|
12
13
|
export const addFileWithFactory = (node, checker, methods = new Map(), expectedTypeName, state) => {
|
|
13
14
|
if (ts.isVariableDeclaration(node)) {
|
|
@@ -277,7 +277,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
277
277
|
let deploy;
|
|
278
278
|
let approvalRequired;
|
|
279
279
|
let approvalDescription;
|
|
280
|
-
let
|
|
280
|
+
let workflowQueued;
|
|
281
|
+
let workflowRetries;
|
|
282
|
+
let workflowTimeout;
|
|
281
283
|
let version;
|
|
282
284
|
let objectNode;
|
|
283
285
|
let nodeDisplayName = null;
|
|
@@ -345,7 +347,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
345
347
|
readonly_ = getPropertyValue(firstArg, 'readonly');
|
|
346
348
|
deploy = getPropertyValue(firstArg, 'deploy');
|
|
347
349
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
|
|
348
|
-
|
|
350
|
+
workflowQueued = getPropertyValue(firstArg, 'workflowQueued');
|
|
351
|
+
workflowRetries = getPropertyValue(firstArg, 'workflowRetries');
|
|
352
|
+
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout');
|
|
349
353
|
// Extract approvalDescription identifier reference
|
|
350
354
|
for (const prop of firstArg.properties) {
|
|
351
355
|
if (ts.isPropertyAssignment(prop) &&
|
|
@@ -740,7 +744,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
740
744
|
deploy: deploy || undefined,
|
|
741
745
|
approvalRequired: approvalRequired || undefined,
|
|
742
746
|
approvalDescription: approvalDescription || undefined,
|
|
743
|
-
|
|
747
|
+
workflowQueued: workflowQueued === true ? true : undefined,
|
|
748
|
+
workflowRetries: workflowRetries ?? undefined,
|
|
749
|
+
workflowTimeout: workflowTimeout ?? undefined,
|
|
744
750
|
implementationHash,
|
|
745
751
|
version,
|
|
746
752
|
title,
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
function hasTypeCast(node) {
|
|
4
|
+
return ts.isAsExpression(node) || ts.isTypeAssertionExpression(node);
|
|
5
|
+
}
|
|
6
|
+
function outerParent(node) {
|
|
7
|
+
let p = node.parent;
|
|
8
|
+
while (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p))) {
|
|
9
|
+
p = p.parent;
|
|
10
|
+
}
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
function findCastArg(args) {
|
|
14
|
+
return args.find(hasTypeCast);
|
|
15
|
+
}
|
|
2
16
|
/**
|
|
3
17
|
* Helper to extract namespace from a namespaced function reference like 'ext:hello'
|
|
4
18
|
*/
|
|
@@ -52,6 +66,19 @@ export function addRPCInvocations(node, state, logger) {
|
|
|
52
66
|
expression.name.text === 'invoke' &&
|
|
53
67
|
ts.isIdentifier(expression.expression) &&
|
|
54
68
|
expression.expression.text === 'rpc') {
|
|
69
|
+
// Skip PKU940 for generated files — they may contain intentional casts
|
|
70
|
+
// (e.g. the paginated useInfiniteQuery hook in pikku-react-query.gen.ts).
|
|
71
|
+
const sourceFileName = node.getSourceFile().fileName;
|
|
72
|
+
const isGenerated = sourceFileName.endsWith('.gen.ts') || sourceFileName.endsWith('.gen.js');
|
|
73
|
+
if (!isGenerated) {
|
|
74
|
+
if (hasTypeCast(outerParent(node))) {
|
|
75
|
+
logger.critical(ErrorCode.RPC_INVOCATION_TYPE_CAST, `rpc.invoke() result is type-cast — remove the 'as' expression and rely on Pikku's generated types`);
|
|
76
|
+
}
|
|
77
|
+
const castArg = findCastArg(args);
|
|
78
|
+
if (castArg) {
|
|
79
|
+
logger.critical(ErrorCode.RPC_INVOCATION_TYPE_CAST, `rpc.invoke() has a type cast on an argument — remove the 'as' expression and rely on Pikku's generated types`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
55
82
|
const [firstArg] = args;
|
|
56
83
|
if (firstArg) {
|
|
57
84
|
if (ts.isStringLiteral(firstArg)) {
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -59,7 +59,8 @@ export declare enum ErrorCode {
|
|
|
59
59
|
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
|
|
60
60
|
PII_IN_OUTPUT = "PKU910",
|
|
61
61
|
ADDON_WIRING_NOT_ALLOWED = "PKU920",
|
|
62
|
-
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921"
|
|
62
|
+
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
|
|
63
|
+
RPC_INVOCATION_TYPE_CAST = "PKU940"
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
65
66
|
* Severity of a tracked, coded diagnostic. `critical` always blocks the build;
|
package/dist/error-codes.js
CHANGED
package/dist/inspector.js
CHANGED
|
@@ -30,7 +30,9 @@ export function getInitialInspectorState(rootDir) {
|
|
|
30
30
|
wireServicesFactories: new Map(),
|
|
31
31
|
wireServicesMeta: new Map(),
|
|
32
32
|
addonRequiredParentServices: [],
|
|
33
|
+
addonServerlessIncompatible: new Map(),
|
|
33
34
|
configFactories: new Map(),
|
|
35
|
+
serverLifecycleFactories: new Map(),
|
|
34
36
|
filesAndMethods: {},
|
|
35
37
|
filesAndMethodsErrors: new Map(),
|
|
36
38
|
typesLookup: new Map(),
|
package/dist/types.d.ts
CHANGED
|
@@ -196,6 +196,7 @@ export type InspectorFilters = {
|
|
|
196
196
|
target?: Array<'serverless' | 'server'>;
|
|
197
197
|
excludeTarget?: Array<'serverless' | 'server'>;
|
|
198
198
|
serverlessIncompatible?: string[];
|
|
199
|
+
defaultTarget?: 'serverless' | 'server';
|
|
199
200
|
};
|
|
200
201
|
export type AddonConfig = {
|
|
201
202
|
package: string;
|
|
@@ -302,6 +303,12 @@ export interface InspectorFilesAndMethods {
|
|
|
302
303
|
type: string;
|
|
303
304
|
typePath: string;
|
|
304
305
|
};
|
|
306
|
+
serverLifecycleFactory?: {
|
|
307
|
+
file: string;
|
|
308
|
+
variable: string;
|
|
309
|
+
type: string;
|
|
310
|
+
typePath: string;
|
|
311
|
+
};
|
|
305
312
|
}
|
|
306
313
|
export interface InspectorDiagnostic {
|
|
307
314
|
code: string;
|
|
@@ -353,7 +360,9 @@ export interface InspectorState {
|
|
|
353
360
|
wireServicesFactories: PathToNameAndType;
|
|
354
361
|
wireServicesMeta: Map<string, string[]>;
|
|
355
362
|
addonRequiredParentServices: string[];
|
|
363
|
+
addonServerlessIncompatible: Map<string, string[]>;
|
|
356
364
|
configFactories: PathToNameAndType;
|
|
365
|
+
serverLifecycleFactories: PathToNameAndType;
|
|
357
366
|
filesAndMethods: InspectorFilesAndMethods;
|
|
358
367
|
filesAndMethodsErrors: Map<string, PathToNameAndType>;
|
|
359
368
|
typesLookup: Map<string, ts.Type[]>;
|
|
@@ -256,9 +256,10 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
256
256
|
? new Set(filters.excludeTarget)
|
|
257
257
|
: null;
|
|
258
258
|
const incompatible = new Set(filters.serverlessIncompatible ?? []);
|
|
259
|
+
const defaultTarget = filters.defaultTarget ?? 'serverless';
|
|
259
260
|
keptByDeploy = new Set();
|
|
260
261
|
for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
|
|
261
|
-
const target = resolveDeployTarget(funcMeta, incompatible, funcId);
|
|
262
|
+
const target = resolveDeployTarget(funcMeta, incompatible, funcId, defaultTarget);
|
|
262
263
|
if (allowed && !allowed.has(target))
|
|
263
264
|
continue;
|
|
264
265
|
if (excluded && excluded.has(target))
|
|
@@ -761,10 +762,10 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
761
762
|
filteredState.requiredSchemas = prunedSchemas;
|
|
762
763
|
}
|
|
763
764
|
// Step dispatch is decided purely per-function: a workflow step runs via the
|
|
764
|
-
// queue only when its function
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
765
|
+
// queue only when its function is marked `workflowQueued: true`. Such a unit
|
|
766
|
+
// needs workflowService + queueService injected even though the function
|
|
767
|
+
// itself doesn't reference them. Check the ORIGINAL graph meta (before
|
|
768
|
+
// filtering pruned it).
|
|
768
769
|
const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
|
|
769
770
|
const resolveFuncId = (rpcName) => filteredState.rpc.internalMeta[rpcName] ??
|
|
770
771
|
filteredState.rpc.exposedMeta[rpcName] ??
|
|
@@ -782,7 +783,7 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
782
783
|
continue;
|
|
783
784
|
const funcMeta = (filteredState.functions.meta[funcId] ??
|
|
784
785
|
filteredState.functions.meta[rpcName]);
|
|
785
|
-
if (funcMeta?.
|
|
786
|
+
if (funcMeta?.workflowQueued === true) {
|
|
786
787
|
filteredState.serviceAggregation.requiredServices.add('workflowService');
|
|
787
788
|
filteredState.serviceAggregation.requiredServices.add('queueService');
|
|
788
789
|
}
|
|
@@ -13,9 +13,10 @@ export type FilesAndMethods = {
|
|
|
13
13
|
pikkuConfigFactory: Meta;
|
|
14
14
|
singletonServicesFactory: Meta;
|
|
15
15
|
wireServicesFactory: Meta;
|
|
16
|
+
serverLifecycleFactory: Meta;
|
|
16
17
|
};
|
|
17
18
|
export type FilesAndMethodsErrors = Map<string, PathToNameAndType>;
|
|
18
|
-
export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, }?: InspectorOptions["types"]) => {
|
|
19
|
+
export declare const getFilesAndMethods: ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, serverLifecycleFactories, }: InspectorState, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, }?: InspectorOptions["types"]) => {
|
|
19
20
|
result: Partial<FilesAndMethods>;
|
|
20
21
|
errors: FilesAndMethodsErrors;
|
|
21
22
|
};
|
|
@@ -46,7 +46,7 @@ const getMetaTypes = (type, map, desiredType, errors) => {
|
|
|
46
46
|
}
|
|
47
47
|
return;
|
|
48
48
|
};
|
|
49
|
-
export const getFilesAndMethods = ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, } = {}) => {
|
|
49
|
+
export const getFilesAndMethods = ({ singletonServicesTypeImportMap, wireServicesTypeImportMap, userSessionTypeImportMap, configTypeImportMap, wireServicesFactories, singletonServicesFactories, configFactories, serverLifecycleFactories, }, { configFileType, userSessionType, singletonServicesFactoryType, wireServicesFactoryType, } = {}) => {
|
|
50
50
|
const errors = new Map();
|
|
51
51
|
const result = {
|
|
52
52
|
userSessionType: getMetaTypes('CoreUserSession', userSessionTypeImportMap, userSessionType, errors),
|
|
@@ -56,6 +56,7 @@ export const getFilesAndMethods = ({ singletonServicesTypeImportMap, wireService
|
|
|
56
56
|
pikkuConfigFactory: getMetaTypes('CoreConfig', configFactories, configFileType, errors),
|
|
57
57
|
singletonServicesFactory: getMetaTypes('CreateSingletonServices', singletonServicesFactories, singletonServicesFactoryType, errors),
|
|
58
58
|
wireServicesFactory: getMetaTypes('CreateWireServices', wireServicesFactories, wireServicesFactoryType, errors),
|
|
59
|
+
serverLifecycleFactory: getMetaTypes('ServerLifecycle', serverLifecycleFactories),
|
|
59
60
|
};
|
|
60
61
|
return { result, errors };
|
|
61
62
|
};
|
|
@@ -134,6 +134,20 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
134
134
|
catch {
|
|
135
135
|
// No variables meta — that's fine
|
|
136
136
|
}
|
|
137
|
+
// Load addon serverlessIncompatible service names from pikku-addon-meta.gen.json
|
|
138
|
+
try {
|
|
139
|
+
const addonMetaPath = require.resolve(`${decl.package}/.pikku/console/pikku-addon-meta.gen.json`);
|
|
140
|
+
const addonMetaRaw = await readFile(addonMetaPath, 'utf-8');
|
|
141
|
+
const addonMeta = JSON.parse(addonMetaRaw);
|
|
142
|
+
if (Array.isArray(addonMeta.serverlessIncompatible) &&
|
|
143
|
+
addonMeta.serverlessIncompatible.length > 0) {
|
|
144
|
+
state.addonServerlessIncompatible.set(namespace, addonMeta.serverlessIncompatible);
|
|
145
|
+
logger.debug(`Addon '${namespace}' marks [${addonMeta.serverlessIncompatible.join(', ')}] as serverless-incompatible`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// No addon meta or no serverlessIncompatible declared — that's fine
|
|
150
|
+
}
|
|
137
151
|
// Load addon required parent services from pikku-services.gen
|
|
138
152
|
try {
|
|
139
153
|
const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
|
|
@@ -220,11 +220,18 @@ export function aggregateRequiredServices(state) {
|
|
|
220
220
|
pikkuFuncId: `pikkuWorkflowOrchestrator:${graph.name}`,
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
|
-
// Per-step queues
|
|
223
|
+
// Per-step queues — only for steps explicitly marked workflowQueued: true
|
|
224
224
|
for (const node of Object.values(graph.nodes)) {
|
|
225
225
|
if (!('rpcName' in node) || !node.rpcName)
|
|
226
226
|
continue;
|
|
227
227
|
const rpcName = node.rpcName;
|
|
228
|
+
const funcId = state.rpc?.internalMeta?.[rpcName] ??
|
|
229
|
+
state.rpc?.exposedMeta?.[rpcName] ??
|
|
230
|
+
rpcName;
|
|
231
|
+
const funcMeta = (state.functions.meta[funcId] ??
|
|
232
|
+
state.functions.meta[rpcName]);
|
|
233
|
+
if (funcMeta?.workflowQueued !== true)
|
|
234
|
+
continue;
|
|
228
235
|
const stepQueueName = `wf-step-${toKebab(rpcName)}`;
|
|
229
236
|
if (!state.queueWorkers.meta[stepQueueName]) {
|
|
230
237
|
state.queueWorkers.meta[stepQueueName] = {
|
|
@@ -18,11 +18,12 @@ export declare class IncompatibleDeployTargetError extends Error {
|
|
|
18
18
|
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
19
19
|
* - otherwise target is 'server'
|
|
20
20
|
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
21
|
-
* 3.
|
|
21
|
+
* 3. `defaultTarget` (sourced from `pikku.config.json` →
|
|
22
|
+
* `deploy.defaultTarget`, falling back to 'serverless')
|
|
22
23
|
*
|
|
23
24
|
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
24
25
|
* into deployment units) and by `filterInspectorState` (when
|
|
25
26
|
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
26
27
|
* of gen files).
|
|
27
28
|
*/
|
|
28
|
-
export declare function resolveDeployTarget(funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>, serverlessIncompatible: Set<string>, functionName?: string): 'serverless' | 'server';
|
|
29
|
+
export declare function resolveDeployTarget(funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>, serverlessIncompatible: Set<string>, functionName?: string, defaultTarget?: 'serverless' | 'server'): 'serverless' | 'server';
|
|
@@ -25,14 +25,15 @@ export class IncompatibleDeployTargetError extends Error {
|
|
|
25
25
|
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
26
26
|
* - otherwise target is 'server'
|
|
27
27
|
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
28
|
-
* 3.
|
|
28
|
+
* 3. `defaultTarget` (sourced from `pikku.config.json` →
|
|
29
|
+
* `deploy.defaultTarget`, falling back to 'serverless')
|
|
29
30
|
*
|
|
30
31
|
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
31
32
|
* into deployment units) and by `filterInspectorState` (when
|
|
32
33
|
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
33
34
|
* of gen files).
|
|
34
35
|
*/
|
|
35
|
-
export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionName = '<unknown>') {
|
|
36
|
+
export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionName = '<unknown>', defaultTarget = 'serverless') {
|
|
36
37
|
// Service compatibility wins over the explicit flag — a serverless
|
|
37
38
|
// bundle of a function that needs (e.g.) node:fs would crash at runtime.
|
|
38
39
|
const incompatibleHits = [];
|
|
@@ -52,5 +53,5 @@ export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionNa
|
|
|
52
53
|
return 'server';
|
|
53
54
|
if (funcMeta.deploy === 'serverless')
|
|
54
55
|
return 'serverless';
|
|
55
|
-
return
|
|
56
|
+
return defaultTarget;
|
|
56
57
|
}
|
|
@@ -56,6 +56,7 @@ export interface SerializableInspectorState {
|
|
|
56
56
|
]>;
|
|
57
57
|
wireServicesMeta: Array<[string, string[]]>;
|
|
58
58
|
addonRequiredParentServices: string[];
|
|
59
|
+
addonServerlessIncompatible: Array<[string, string[]]>;
|
|
59
60
|
configFactories: Array<[
|
|
60
61
|
string,
|
|
61
62
|
{
|
|
@@ -64,6 +65,14 @@ export interface SerializableInspectorState {
|
|
|
64
65
|
typePath: string | null;
|
|
65
66
|
}[]
|
|
66
67
|
]>;
|
|
68
|
+
serverLifecycleFactories: Array<[
|
|
69
|
+
string,
|
|
70
|
+
{
|
|
71
|
+
variable: string;
|
|
72
|
+
type: string | null;
|
|
73
|
+
typePath: string | null;
|
|
74
|
+
}[]
|
|
75
|
+
]>;
|
|
67
76
|
filesAndMethods: InspectorState['filesAndMethods'];
|
|
68
77
|
filesAndMethodsErrors: Array<[
|
|
69
78
|
string,
|
|
@@ -24,7 +24,9 @@ export function serializeInspectorState(state) {
|
|
|
24
24
|
wireServicesFactories: Array.from(state.wireServicesFactories.entries()),
|
|
25
25
|
wireServicesMeta: Array.from(state.wireServicesMeta.entries()),
|
|
26
26
|
addonRequiredParentServices: state.addonRequiredParentServices,
|
|
27
|
+
addonServerlessIncompatible: Array.from(state.addonServerlessIncompatible.entries()),
|
|
27
28
|
configFactories: Array.from(state.configFactories.entries()),
|
|
29
|
+
serverLifecycleFactories: Array.from(state.serverLifecycleFactories.entries()),
|
|
28
30
|
filesAndMethods: state.filesAndMethods,
|
|
29
31
|
filesAndMethodsErrors: Array.from(state.filesAndMethodsErrors.entries()).map(([key, mapValue]) => [key, Array.from(mapValue.entries())]),
|
|
30
32
|
schemaLookup: Array.from(state.schemaLookup.entries()),
|
|
@@ -176,7 +178,9 @@ export function deserializeInspectorState(data) {
|
|
|
176
178
|
wireServicesFactories: new Map(data.wireServicesFactories),
|
|
177
179
|
wireServicesMeta: new Map(data.wireServicesMeta),
|
|
178
180
|
addonRequiredParentServices: data.addonRequiredParentServices || [],
|
|
181
|
+
addonServerlessIncompatible: new Map(data.addonServerlessIncompatible || []),
|
|
179
182
|
configFactories: new Map(data.configFactories),
|
|
183
|
+
serverLifecycleFactories: new Map(data.serverLifecycleFactories),
|
|
180
184
|
filesAndMethods: data.filesAndMethods,
|
|
181
185
|
filesAndMethodsErrors: new Map(data.filesAndMethodsErrors.map(([key, entries]) => [
|
|
182
186
|
key,
|
|
@@ -43,6 +43,15 @@ function containsLoop(steps) {
|
|
|
43
43
|
function containsConditional(steps) {
|
|
44
44
|
return steps.some((step) => step.type === 'branch' || step.type === 'switch');
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Turn an internal snake/kebab reason key into a sentence-case display label.
|
|
48
|
+
* e.g. "awaiting_approval" → "Awaiting approval", "building-image" → "Building image"
|
|
49
|
+
*/
|
|
50
|
+
function reasonToLabel(reason) {
|
|
51
|
+
return reason
|
|
52
|
+
.replace(/[-_]/g, ' ')
|
|
53
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
54
|
+
}
|
|
46
55
|
/** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
|
|
47
56
|
function collectNamedSteps(steps) {
|
|
48
57
|
const planned = [];
|
|
@@ -53,6 +62,16 @@ function collectNamedSteps(steps) {
|
|
|
53
62
|
case 'sleep':
|
|
54
63
|
planned.push({ stepName: step.stepName });
|
|
55
64
|
break;
|
|
65
|
+
case 'suspend':
|
|
66
|
+
// Runtime stores the suspend step as `__workflow_suspend:${reason}`.
|
|
67
|
+
// We surface it in the planned ladder with a readable displayName so
|
|
68
|
+
// the UI shows it in the right position instead of appending it at the
|
|
69
|
+
// bottom as an unrecognised orphan.
|
|
70
|
+
planned.push({
|
|
71
|
+
stepName: `__workflow_suspend:${step.reason}`,
|
|
72
|
+
displayName: reasonToLabel(step.reason),
|
|
73
|
+
});
|
|
74
|
+
break;
|
|
56
75
|
case 'parallel':
|
|
57
76
|
for (const child of step.children) {
|
|
58
77
|
planned.push({ stepName: child.stepName });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, 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
|
/**
|
|
@@ -360,6 +360,9 @@ function extractExpressionStatement(statement, context) {
|
|
|
360
360
|
if (isWorkflowSleepCall(call, context.checker)) {
|
|
361
361
|
return extractSleepStep(call, context);
|
|
362
362
|
}
|
|
363
|
+
if (isWorkflowSuspendCall(call, context.checker)) {
|
|
364
|
+
return extractSuspendStep(call, context);
|
|
365
|
+
}
|
|
363
366
|
// Check for parallel group or fanout
|
|
364
367
|
if (isParallelFanout(call)) {
|
|
365
368
|
return extractParallelFanout(call, context);
|
|
@@ -513,6 +516,28 @@ function extractSleepStep(call, context) {
|
|
|
513
516
|
return null;
|
|
514
517
|
}
|
|
515
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Extract suspend step from workflow.suspend() call
|
|
521
|
+
*/
|
|
522
|
+
function extractSuspendStep(call, context) {
|
|
523
|
+
const args = call.arguments;
|
|
524
|
+
if (args.length < 1)
|
|
525
|
+
return null;
|
|
526
|
+
try {
|
|
527
|
+
const reason = extractStringLiteral(args[0], context.checker);
|
|
528
|
+
return {
|
|
529
|
+
type: 'suspend',
|
|
530
|
+
reason,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
context.errors.push({
|
|
535
|
+
message: `Failed to extract suspend step: ${error instanceof Error ? error.message : String(error)}`,
|
|
536
|
+
node: call,
|
|
537
|
+
});
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
516
541
|
/**
|
|
517
542
|
* Extract cancel step from throw WorkflowCancelledException statement
|
|
518
543
|
*/
|
|
@@ -10,6 +10,10 @@ export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.Ty
|
|
|
10
10
|
* Check if a call expression is workflow.sleep()
|
|
11
11
|
*/
|
|
12
12
|
export declare function isWorkflowSleepCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a call expression is workflow.suspend()
|
|
15
|
+
*/
|
|
16
|
+
export declare function isWorkflowSuspendCall(node: ts.CallExpression, _checker: ts.TypeChecker): boolean;
|
|
13
17
|
/**
|
|
14
18
|
* Check if a throw statement throws WorkflowCancelledException
|
|
15
19
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|
|
@@ -26,6 +26,18 @@ export function isWorkflowSleepCall(node, checker) {
|
|
|
26
26
|
ts.isIdentifier(propAccess.expression) &&
|
|
27
27
|
propAccess.expression.text === 'workflow');
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a call expression is workflow.suspend()
|
|
31
|
+
*/
|
|
32
|
+
export function isWorkflowSuspendCall(node, _checker) {
|
|
33
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const propAccess = node.expression;
|
|
37
|
+
return (propAccess.name.text === 'suspend' &&
|
|
38
|
+
ts.isIdentifier(propAccess.expression) &&
|
|
39
|
+
propAccess.expression.text === 'workflow');
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* Check if a throw statement throws WorkflowCancelledException
|
|
31
43
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|
|
@@ -62,8 +62,6 @@ export interface NodeOptions {
|
|
|
62
62
|
retryDelay?: string;
|
|
63
63
|
/** Timeout for node execution (e.g., '30s', '5m') */
|
|
64
64
|
timeout?: string;
|
|
65
|
-
/** If true, execute via queue (async). Default: false (inline) */
|
|
66
|
-
async?: boolean;
|
|
67
65
|
}
|
|
68
66
|
/**
|
|
69
67
|
* Flow node types for control flow (no RPC call)
|
package/dist/visit.js
CHANGED
|
@@ -33,6 +33,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
33
33
|
addFileWithFactory(node, checker, state.singletonServicesFactories, 'CreateSingletonServices', state);
|
|
34
34
|
addFileWithFactory(node, checker, state.wireServicesFactories, 'CreateWireServices', state);
|
|
35
35
|
addFileWithFactory(node, checker, state.configFactories, 'CreateConfig');
|
|
36
|
+
addFileWithFactory(node, checker, state.serverLifecycleFactories, 'ServerLifecycle');
|
|
36
37
|
addRPCInvocations(node, state, logger);
|
|
37
38
|
addWireAddon(node, state, logger);
|
|
38
39
|
addMiddleware(logger, node, checker, state, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.29",
|
|
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.42",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "^2.5.0",
|
|
@@ -10,6 +10,7 @@ const wrapperFunctionMap: Record<string, string> = {
|
|
|
10
10
|
pikkuAddonServices: 'CreateSingletonServices',
|
|
11
11
|
pikkuWireServices: 'CreateWireServices',
|
|
12
12
|
pikkuAddonWireServices: 'CreateWireServices',
|
|
13
|
+
pikkuServerLifecycle: 'ServerLifecycle',
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export const addFileWithFactory = (
|
package/src/add/add-functions.ts
CHANGED
|
@@ -392,7 +392,9 @@ export const addFunctions: AddWiring = (
|
|
|
392
392
|
let deploy: 'serverless' | 'server' | 'auto' | undefined
|
|
393
393
|
let approvalRequired: boolean | undefined
|
|
394
394
|
let approvalDescription: string | undefined
|
|
395
|
-
let
|
|
395
|
+
let workflowQueued: boolean | undefined
|
|
396
|
+
let workflowRetries: number | undefined
|
|
397
|
+
let workflowTimeout: string | undefined
|
|
396
398
|
let version: number | undefined
|
|
397
399
|
let objectNode: ts.ObjectLiteralExpression | undefined
|
|
398
400
|
let nodeDisplayName: string | null = null
|
|
@@ -488,7 +490,9 @@ export const addFunctions: AddWiring = (
|
|
|
488
490
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
489
491
|
| boolean
|
|
490
492
|
| undefined
|
|
491
|
-
|
|
493
|
+
workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as boolean | undefined
|
|
494
|
+
workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as number | undefined
|
|
495
|
+
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as string | undefined
|
|
492
496
|
|
|
493
497
|
// Extract approvalDescription identifier reference
|
|
494
498
|
for (const prop of firstArg.properties) {
|
|
@@ -1027,7 +1031,9 @@ export const addFunctions: AddWiring = (
|
|
|
1027
1031
|
deploy: deploy || undefined,
|
|
1028
1032
|
approvalRequired: approvalRequired || undefined,
|
|
1029
1033
|
approvalDescription: approvalDescription || undefined,
|
|
1030
|
-
|
|
1034
|
+
workflowQueued: workflowQueued === true ? true : undefined,
|
|
1035
|
+
workflowRetries: workflowRetries ?? undefined,
|
|
1036
|
+
workflowTimeout: workflowTimeout ?? undefined,
|
|
1031
1037
|
implementationHash,
|
|
1032
1038
|
version,
|
|
1033
1039
|
title,
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
3
|
+
import { ErrorCode } from '../error-codes.js'
|
|
4
|
+
|
|
5
|
+
function hasTypeCast(node: ts.Node): boolean {
|
|
6
|
+
return ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function outerParent(node: ts.Node): ts.Node {
|
|
10
|
+
let p = node.parent
|
|
11
|
+
while (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p))) {
|
|
12
|
+
p = p.parent
|
|
13
|
+
}
|
|
14
|
+
return p
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findCastArg(
|
|
18
|
+
args: ts.NodeArray<ts.Expression>
|
|
19
|
+
): ts.Expression | undefined {
|
|
20
|
+
return args.find(hasTypeCast)
|
|
21
|
+
}
|
|
3
22
|
|
|
4
23
|
/**
|
|
5
24
|
* Helper to extract namespace from a namespaced function reference like 'ext:hello'
|
|
@@ -67,6 +86,28 @@ export function addRPCInvocations(
|
|
|
67
86
|
ts.isIdentifier(expression.expression) &&
|
|
68
87
|
expression.expression.text === 'rpc'
|
|
69
88
|
) {
|
|
89
|
+
// Skip PKU940 for generated files — they may contain intentional casts
|
|
90
|
+
// (e.g. the paginated useInfiniteQuery hook in pikku-react-query.gen.ts).
|
|
91
|
+
const sourceFileName = node.getSourceFile().fileName
|
|
92
|
+
const isGenerated =
|
|
93
|
+
sourceFileName.endsWith('.gen.ts') || sourceFileName.endsWith('.gen.js')
|
|
94
|
+
if (!isGenerated) {
|
|
95
|
+
if (hasTypeCast(outerParent(node))) {
|
|
96
|
+
logger.critical(
|
|
97
|
+
ErrorCode.RPC_INVOCATION_TYPE_CAST,
|
|
98
|
+
`rpc.invoke() result is type-cast — remove the 'as' expression and rely on Pikku's generated types`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const castArg = findCastArg(args)
|
|
103
|
+
if (castArg) {
|
|
104
|
+
logger.critical(
|
|
105
|
+
ErrorCode.RPC_INVOCATION_TYPE_CAST,
|
|
106
|
+
`rpc.invoke() has a type cast on an argument — remove the 'as' expression and rely on Pikku's generated types`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
const [firstArg] = args
|
|
71
112
|
if (firstArg) {
|
|
72
113
|
if (ts.isStringLiteral(firstArg)) {
|