@pikku/inspector 0.12.26 → 0.12.28
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 +115 -0
- package/dist/add/add-functions.js +15 -4
- 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 +18 -3
- package/dist/types.d.ts +14 -0
- package/dist/utils/extract-function-name.js +20 -3
- package/dist/utils/filter-inspector-state.js +7 -6
- 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/schema-generator.d.ts +1 -0
- package/dist/utils/schema-generator.js +76 -0
- package/dist/utils/workflow/derive-workflow-plan.d.ts +20 -0
- package/dist/utils/workflow/derive-workflow-plan.js +78 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +15 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +14 -3
- package/dist/visit.d.ts +1 -0
- package/dist/visit.js +14 -1
- package/package.json +2 -2
- package/src/add/add-functions.ts +15 -4
- package/src/add/add-rpc-invocations.ts +41 -0
- package/src/add/pii-check.test.ts +3 -1
- package/src/add/rpc-type-cast.test.ts +123 -0
- package/src/error-codes.ts +2 -0
- package/src/inspector.ts +24 -2
- package/src/types.ts +17 -0
- package/src/utils/extract-function-name.ts +21 -3
- package/src/utils/filter-inspector-state.ts +13 -7
- 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/schema-generator.ts +92 -0
- package/src/utils/workflow/derive-workflow-plan.test.ts +122 -0
- package/src/utils/workflow/derive-workflow-plan.ts +90 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +15 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +18 -3
- package/src/visit.ts +25 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
|
1
|
+
## 0.12.28
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 66d43d1: Add `deploy.defaultTarget` to `pikku.config.json` to override the default deploy target ('serverless') for functions without an explicit `deploy` flag.
|
|
6
|
+
- a8c9e6d: feat(inspector): add PKU940 — block type casts on rpc.invoke() calls
|
|
7
|
+
|
|
8
|
+
The inspector now emits a critical PKU940 error when `rpc.invoke()` is called
|
|
9
|
+
with an `as` cast on an argument (`rpc.invoke('fn', data as any)`) or when its
|
|
10
|
+
result is cast (`rpc.invoke('fn', data) as any`). Both patterns defeat Pikku's
|
|
11
|
+
generated type safety and are rejected at build time.
|
|
12
|
+
|
|
13
|
+
- ba1ab08: refactor(workflow): replace `inline: false` with `workflowQueued: true` on function meta
|
|
14
|
+
|
|
15
|
+
The per-function workflow dispatch flag has been renamed from the confusing
|
|
16
|
+
negative `inline: false` to the explicit positive `workflowQueued: true`.
|
|
17
|
+
Two companion fields are also added: `workflowRetries` and `workflowTimeout`
|
|
18
|
+
as function-level equivalents of the per-call-site `NodeOptions` fields.
|
|
19
|
+
|
|
20
|
+
**Breaking change (patch — flag was undocumented):** rename `inline: false`
|
|
21
|
+
to `workflowQueued: true` on any `pikkuSessionlessFunc` / `pikkuFunc` that
|
|
22
|
+
dispatches its workflow steps via the queue.
|
|
23
|
+
|
|
24
|
+
**Behaviour change:** a step marked `workflowQueued: true` now throws if no
|
|
25
|
+
queue service is configured, instead of silently falling back to inline
|
|
26
|
+
execution.
|
|
27
|
+
|
|
28
|
+
**Bug fix:** `post-process.ts` was registering `wf-step-*` queues for every
|
|
29
|
+
workflow step node; it now only registers them for steps that are actually
|
|
30
|
+
`workflowQueued: true`, avoiding spurious queue resource allocation.
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [ba1ab08]
|
|
33
|
+
- @pikku/core@0.12.40
|
|
34
|
+
|
|
35
|
+
## 0.12.27
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 41ff485: fix(inspector): register functions in a dedicated pass before wiring resolution
|
|
40
|
+
|
|
41
|
+
The deterministic-codegen change sorted `program.getSourceFiles()` so generated
|
|
42
|
+
output is byte-identical across runs. But function registration (`addFunctions`)
|
|
43
|
+
ran in the same sweep as wiring resolution (`visitRoutes`), so once traversal
|
|
44
|
+
became alphabetical, a wiring file could be visited before the file that defines
|
|
45
|
+
the function it references — e.g. an addon contract (`hello.contracts.ts`)
|
|
46
|
+
before `hello.functions.ts` — producing a spurious `PKU559` ("No function
|
|
47
|
+
metadata found for channel handler").
|
|
48
|
+
|
|
49
|
+
Function registration now runs in its own pass (`visitFunctions`) over the
|
|
50
|
+
sorted files, completing before any transport/wiring resolution, so resolution
|
|
51
|
+
no longer depends on source-file order. Also sort the `register.gen.ts` schema
|
|
52
|
+
imports (driven by a `Set`) so that file is stable too, and opt the PII-check
|
|
53
|
+
tests into the now-opt-in classification scan.
|
|
54
|
+
|
|
55
|
+
- d2078c8: fix(inspector): make codegen output deterministic across runs
|
|
56
|
+
|
|
57
|
+
Two sources of non-reproducible `pikku all` output are fixed:
|
|
58
|
+
1. **Random placeholder ids.** Anonymous/unnamed functions and inline (non-exported) permissions were given a `__temp_${randomUUID()}` id, so a referenced-but-not-exported `pikkuPermission` const (e.g. `permissions: { admin: [requiresPlatformAdmin] }`) produced a fresh UUID in the generated meta on every run. The placeholder is now derived deterministically from the call expression's source location (relative path + start offset), still `__temp_`-prefixed so downstream name resolution is unchanged.
|
|
59
|
+
2. **Unstable file-traversal order.** The two inspector sweeps iterated `program.getSourceFiles()` in glob + import-graph order, which varies run to run, so meta keys (and anything serialized in insertion order) were emitted in a different order each time — making a plain `git diff` of generated files look like functions were appearing/vanishing when the set was identical. Source files are now sorted by file name before the sweeps.
|
|
60
|
+
|
|
61
|
+
Net effect: byte-identical generated output across repeated runs with no source changes (verified across the full `.pikku` tree of a 331-function project).
|
|
62
|
+
|
|
63
|
+
- e6fd12b: perf(inspector,cli): persist generated TS schemas to disk across runs
|
|
64
|
+
|
|
65
|
+
`generateAllSchemas` already cached its `ts-json-schema-generator` output
|
|
66
|
+
in-memory (keyed by the generated custom-types content), so the 2nd and 3rd
|
|
67
|
+
inspector passes within a single `pikku all` were near-free. But the cache
|
|
68
|
+
never survived the process, so every fresh `pikku all` paid the full cold cost
|
|
69
|
+
of building a second TS program + running ts-json-schema-generator — on a
|
|
70
|
+
331-function project that's ~2.2s, the single largest line item of a run.
|
|
71
|
+
|
|
72
|
+
The cache is now also persisted to disk (`node_modules/.cache/pikku/ts-schemas.json`,
|
|
73
|
+
gitignored by convention), keyed by a hash of the custom-types content plus the
|
|
74
|
+
generator options that affect output. A warm `pikku all` whose function types
|
|
75
|
+
are unchanged loads the schemas from disk and skips schema generation entirely;
|
|
76
|
+
the cold first pass drops by ~3.4s in practice (it also primes the in-memory
|
|
77
|
+
cache for the re-inspect passes). Zod schemas are still regenerated every run
|
|
78
|
+
(already ~1ms each). Output is byte-identical to a cold run (verified across the
|
|
79
|
+
full generated tree). The key is derived from the same content the in-memory
|
|
80
|
+
cache uses, so any type change busts it. It also folds in the `@pikku/inspector`
|
|
81
|
+
package version, so upgrading the inspector (the channel a schema-format change
|
|
82
|
+
ships through) auto-invalidates every cache; `SCHEMA_CACHE_VERSION` remains a
|
|
83
|
+
manual lever for in-development format changes between releases.
|
|
84
|
+
|
|
85
|
+
Opt-out: omit `schemaConfig.cacheDir` (the CLI sets it by default).
|
|
86
|
+
|
|
87
|
+
- 244d892: perf(cli,inspector): make the data-classification scan opt-in (`pikku all --security`)
|
|
88
|
+
|
|
89
|
+
`pikku all` was spending the bulk of its wall-clock on the data-classification
|
|
90
|
+
leak check. For every function, on every inspector pass, it called
|
|
91
|
+
`checker.getReturnTypeOfSignature` to infer the handler's return type and scan it
|
|
92
|
+
for `Private`/`Pii`/`Secret` brands — the single most expensive type-checker
|
|
93
|
+
operation. On a 331-function project that was ~7.3s (≈half the total), repeated
|
|
94
|
+
across all three inspector passes, even though the scan only emits diagnostics
|
|
95
|
+
and never affects generated output.
|
|
96
|
+
|
|
97
|
+
The scan is a security lint, not codegen, so it's now **off by default** and gated
|
|
98
|
+
behind a new `--security` flag (or `security: true` in the config). A plain
|
|
99
|
+
`pikku all` skips return-type inference entirely; run `pikku all --security`
|
|
100
|
+
(optionally with `--fail-on-error`) in CI/pre-deploy to enforce it. On the
|
|
101
|
+
331-function project this cut `pikku all` from ~15.3s to ~9.6s.
|
|
102
|
+
|
|
103
|
+
Also: the `all` command now reads back the run's recorded per-step durations and,
|
|
104
|
+
under `PIKKU_TIMING=1`, prints a slowest-first timing table — making it easy to
|
|
105
|
+
see where codegen time goes without adding any hot-path instrumentation.
|
|
106
|
+
|
|
107
|
+
- 940c253: Populate `plannedSteps` and `deterministic` on serialized DSL workflow graphs. For a DSL workflow with no loops (fanout), the inspector now records every named step in source order, so a UI can render the run's step skeleton up front without executing it or hand-listing steps. `deterministic` is `true` only for a flat, loopless, branch-free workflow (exact sequence known ahead of time); a branchy-but-loopless workflow lists all possible steps with `deterministic: false`; any fanout makes the count runtime-dependent so neither field is emitted (just `deterministic: false`). Only `source: 'dsl'` workflows are planned — `complex` step trees omit inline branches and flatten loops, so their plans would misreport determinism. The runtime already threads these fields from workflow meta onto each run via `getRun`.
|
|
108
|
+
- Updated dependencies [4be205f]
|
|
109
|
+
- Updated dependencies [061c717]
|
|
110
|
+
- Updated dependencies [2c55e13]
|
|
111
|
+
- Updated dependencies [c745c26]
|
|
112
|
+
- Updated dependencies [57900b5]
|
|
113
|
+
- Updated dependencies [72694f6]
|
|
114
|
+
- @pikku/core@0.12.39
|
|
115
|
+
|
|
1
116
|
## 0.12.26
|
|
2
117
|
|
|
3
118
|
### Patch Changes
|
|
@@ -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) &&
|
|
@@ -647,7 +651,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
647
651
|
// secret → never returned by any exposed function (sessioned or not)
|
|
648
652
|
// private → only visible to authenticated (sessioned) users; ok for pikkuFunc
|
|
649
653
|
// public → safe for sessionless functions
|
|
650
|
-
|
|
654
|
+
// Opt-in only: inferring every handler's return type (getReturnTypeOfSignature)
|
|
655
|
+
// is the single most expensive checker operation and dominates `pikku all`
|
|
656
|
+
// wall-clock. The classification leak scan is a security lint, not codegen, so
|
|
657
|
+
// it runs ONLY when explicitly requested (`pikku all --security`) — see the
|
|
658
|
+
// classificationCheck option. Default codegen skips it entirely.
|
|
659
|
+
if (options.classificationCheck) {
|
|
651
660
|
const sig = checker.getSignatureFromDeclaration(handler);
|
|
652
661
|
if (sig) {
|
|
653
662
|
const rawRet = checker.getReturnTypeOfSignature(sig);
|
|
@@ -735,7 +744,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
735
744
|
deploy: deploy || undefined,
|
|
736
745
|
approvalRequired: approvalRequired || undefined,
|
|
737
746
|
approvalDescription: approvalDescription || undefined,
|
|
738
|
-
|
|
747
|
+
workflowQueued: workflowQueued === true ? true : undefined,
|
|
748
|
+
workflowRetries: workflowRetries ?? undefined,
|
|
749
|
+
workflowTimeout: workflowTimeout ?? undefined,
|
|
739
750
|
implementationHash,
|
|
740
751
|
version,
|
|
741
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { performance } from 'perf_hooks';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
-
import { visitSetup, visitRoutes } from './visit.js';
|
|
4
|
+
import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
|
|
5
5
|
import { TypesMap } from './types-map.js';
|
|
6
6
|
import { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
7
7
|
import { findCommonAncestor } from './utils/find-root-dir.js';
|
|
@@ -222,10 +222,17 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
222
222
|
// node_modules under rootDir (e.g. a locally-installed addon) is a
|
|
223
223
|
// dependency, not project source — scanning it double-counts the addon's
|
|
224
224
|
// own application types (CoreConfig/Services/SingletonServices).
|
|
225
|
+
// Sort by file name so the sweeps populate state in a stable order. The
|
|
226
|
+
// program's own file order depends on glob + import-graph resolution, which
|
|
227
|
+
// varies run to run — leaving generated meta keys (and anything serialized
|
|
228
|
+
// in insertion order) non-reproducible across identical `pikku all` runs.
|
|
229
|
+
// Safe because function registration is a dedicated pass (visitFunctions)
|
|
230
|
+
// that completes before any order-sensitive wiring resolution in visitRoutes.
|
|
225
231
|
const sourceFiles = program
|
|
226
232
|
.getSourceFiles()
|
|
227
233
|
.filter((sf) => sf.fileName.startsWith(rootDir) &&
|
|
228
|
-
!sf.fileName.includes('/node_modules/'))
|
|
234
|
+
!sf.fileName.includes('/node_modules/'))
|
|
235
|
+
.sort((a, b) => a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0);
|
|
229
236
|
logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
|
|
230
237
|
const state = getInitialInspectorState(rootDir);
|
|
231
238
|
// First sweep: add all functions
|
|
@@ -238,7 +245,15 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
238
245
|
// Load addon function metadata so wirings can reference addon functions
|
|
239
246
|
await loadAddonFunctionsMeta(logger, state);
|
|
240
247
|
if (!options.setupOnly) {
|
|
241
|
-
//
|
|
248
|
+
// Function sweep: register every function before transports/wirings resolve
|
|
249
|
+
// them, so resolution doesn't depend on source-file order.
|
|
250
|
+
const startFunctions = performance.now();
|
|
251
|
+
for (const sourceFile of sourceFiles) {
|
|
252
|
+
const sourceOptions = { ...options, sourceFile };
|
|
253
|
+
ts.forEachChild(sourceFile, (child) => visitFunctions(logger, checker, child, state, sourceOptions));
|
|
254
|
+
}
|
|
255
|
+
logger.debug(`Visit functions phase completed in ${(performance.now() - startFunctions).toFixed(0)}ms`);
|
|
256
|
+
// Transport sweep: add all transports/wirings
|
|
242
257
|
const startRoutes = performance.now();
|
|
243
258
|
for (const sourceFile of sourceFiles) {
|
|
244
259
|
const sourceOptions = { ...options, sourceFile };
|
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;
|
|
@@ -220,6 +221,13 @@ export type InspectorOptions = Partial<{
|
|
|
220
221
|
schema?: {
|
|
221
222
|
additionalProperties?: boolean;
|
|
222
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* Directory for the on-disk TS-schema cache. When set, generated TS schemas
|
|
226
|
+
* are persisted here keyed by a hash of the custom-types content, so a warm
|
|
227
|
+
* `pikku all` whose function types are unchanged skips ts-json-schema-generator
|
|
228
|
+
* entirely (the single largest cold-run cost). Omit to disable disk caching.
|
|
229
|
+
*/
|
|
230
|
+
cacheDir?: string;
|
|
223
231
|
};
|
|
224
232
|
openAPI: {
|
|
225
233
|
additionalInfo: OpenAPISpecInfo;
|
|
@@ -227,6 +235,12 @@ export type InspectorOptions = Partial<{
|
|
|
227
235
|
tags: string[];
|
|
228
236
|
manifest: VersionManifest;
|
|
229
237
|
oldProgram: ts.Program | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* Run the data-classification leak scan (Private/Pii/Secret brands in function
|
|
240
|
+
* return types). Off by default — it forces return-type inference on every
|
|
241
|
+
* function, which is expensive. Enabled via `pikku all --security`.
|
|
242
|
+
*/
|
|
243
|
+
classificationCheck: boolean;
|
|
230
244
|
}>;
|
|
231
245
|
export interface InspectorLogger {
|
|
232
246
|
info: (message: string) => void;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { relative } from 'path';
|
|
3
4
|
import { formatVersionedId } from '@pikku/core';
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic placeholder id for an anonymous/unnamed pikku function or
|
|
7
|
+
* permission. Derived from the call expression's source location (relative path
|
|
8
|
+
* + start position) so `pikku all` produces byte-identical output across runs —
|
|
9
|
+
* a `randomUUID()` here made generated meta non-reproducible. Still `__temp_`
|
|
10
|
+
* prefixed so downstream resolution (which keys off that prefix) is unchanged.
|
|
11
|
+
*/
|
|
12
|
+
function tempFuncId(callExpr, rootDir) {
|
|
13
|
+
const sourceFile = callExpr.getSourceFile();
|
|
14
|
+
const relPath = relative(rootDir, sourceFile.fileName);
|
|
15
|
+
const hash = createHash('sha1')
|
|
16
|
+
.update(`${relPath}:${callExpr.getStart()}`)
|
|
17
|
+
.digest('hex')
|
|
18
|
+
.slice(0, 16);
|
|
19
|
+
return `__temp_${hash}`;
|
|
20
|
+
}
|
|
4
21
|
export function makeContextBasedId(wiringType, ...segments) {
|
|
5
22
|
return [wiringType, ...segments].join(':');
|
|
6
23
|
}
|
|
@@ -88,7 +105,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
88
105
|
result.pikkuFuncId = decl.name.text;
|
|
89
106
|
}
|
|
90
107
|
if (!result.pikkuFuncId) {
|
|
91
|
-
result.pikkuFuncId =
|
|
108
|
+
result.pikkuFuncId = tempFuncId(callExpr, rootDir);
|
|
92
109
|
}
|
|
93
110
|
populateNameByPriority(result);
|
|
94
111
|
return result;
|
|
@@ -335,7 +352,7 @@ export function extractFunctionName(callExpr, checker, rootDir) {
|
|
|
335
352
|
result.pikkuFuncId = result.exportedName;
|
|
336
353
|
}
|
|
337
354
|
else {
|
|
338
|
-
result.pikkuFuncId =
|
|
355
|
+
result.pikkuFuncId = tempFuncId(callExpr, rootDir);
|
|
339
356
|
}
|
|
340
357
|
if (result.version !== null) {
|
|
341
358
|
// Strip trailing VN suffix if it matches the version (e.g. createCardV1 + version:1 → createCard@v1)
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
4
|
import { dirname, join, resolve } from 'path';
|
|
3
5
|
import { createGenerator, RootlessError } from 'ts-json-schema-generator';
|
|
4
6
|
import { register } from 'tsx/esm/api';
|
|
@@ -55,6 +57,61 @@ let cachedParsedConfig;
|
|
|
55
57
|
let cachedTsconfigPath;
|
|
56
58
|
let cachedCustomTypesContent;
|
|
57
59
|
let cachedTSSchemas;
|
|
60
|
+
const SCHEMA_CACHE_VERSION = 1;
|
|
61
|
+
// This package's own version — folded into the cache key so that upgrading
|
|
62
|
+
// @pikku/inspector (the channel through which a schema-format change ships)
|
|
63
|
+
// auto-invalidates every on-disk cache, without relying on someone remembering
|
|
64
|
+
// to bump SCHEMA_CACHE_VERSION. Read once; falls back to the constant if the
|
|
65
|
+
// package.json can't be located (e.g. an unexpected bundling layout).
|
|
66
|
+
const inspectorVersion = (() => {
|
|
67
|
+
try {
|
|
68
|
+
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
69
|
+
const pkg = JSON.parse(readFileSync(pkgUrl, 'utf-8'));
|
|
70
|
+
return typeof pkg.version === 'string' ? pkg.version : `v${SCHEMA_CACHE_VERSION}`;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return `v${SCHEMA_CACHE_VERSION}`;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
// Key the TS-schema cache on everything that affects its output: the generated
|
|
77
|
+
// custom-types source, the generator options that change schema shape, and the
|
|
78
|
+
// inspector version (schema-format changes ship with a version bump).
|
|
79
|
+
function tsSchemaCacheKey(customTypesContent, config) {
|
|
80
|
+
return createHash('sha1')
|
|
81
|
+
.update(`v${SCHEMA_CACHE_VERSION}\0`)
|
|
82
|
+
.update(`pkg:${inspectorVersion}\0`)
|
|
83
|
+
.update(`ap:${config.schema?.additionalProperties ? 1 : 0}\0`)
|
|
84
|
+
.update(`ft:${(config.schemasFromTypes ?? []).join(',')}\0`)
|
|
85
|
+
.update(customTypesContent)
|
|
86
|
+
.digest('hex');
|
|
87
|
+
}
|
|
88
|
+
function schemaCacheFile(cacheDir) {
|
|
89
|
+
return join(cacheDir, 'ts-schemas.json');
|
|
90
|
+
}
|
|
91
|
+
function readDiskTSSchemas(logger, cacheDir, key) {
|
|
92
|
+
const file = schemaCacheFile(cacheDir);
|
|
93
|
+
if (!existsSync(file))
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
97
|
+
if (parsed?.key === key && parsed.schemas)
|
|
98
|
+
return parsed.schemas;
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
logger.debug(`Ignoring unreadable TS-schema cache: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function writeDiskTSSchemas(logger, cacheDir, key, schemas) {
|
|
106
|
+
const file = schemaCacheFile(cacheDir);
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
109
|
+
writeFileSync(file, JSON.stringify({ key, schemas }));
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
logger.debug(`Failed to persist TS-schema cache: ${e.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
58
115
|
function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
|
|
59
116
|
const configPath = resolve(tsconfig);
|
|
60
117
|
// Cache the parsed tsconfig — it doesn't change between runs
|
|
@@ -330,12 +387,31 @@ export async function generateAllSchemas(logger, config, state) {
|
|
|
330
387
|
const zodSchemas = await generateZodSchemas(logger, state.schemaLookup, state.functions.typesMap);
|
|
331
388
|
const requiredTypes = new Set();
|
|
332
389
|
const customTypesContent = generateCustomTypes(state.functions.typesMap, requiredTypes);
|
|
390
|
+
// Fast path: same process, types unchanged — reuse the in-memory result.
|
|
333
391
|
if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
|
|
334
392
|
logger.debug('Reusing cached TS schemas (types unchanged)');
|
|
335
393
|
return { ...cachedTSSchemas, ...zodSchemas };
|
|
336
394
|
}
|
|
395
|
+
// Disk path: a prior `pikku all` left a cache whose key matches the current
|
|
396
|
+
// custom types — load it and skip ts-json-schema-generator (the dominant
|
|
397
|
+
// cold-run cost). Zod schemas are always regenerated (cheap, ~1ms/schema).
|
|
398
|
+
const cacheKey = config.cacheDir
|
|
399
|
+
? tsSchemaCacheKey(customTypesContent, config)
|
|
400
|
+
: null;
|
|
401
|
+
if (config.cacheDir && cacheKey) {
|
|
402
|
+
const disk = readDiskTSSchemas(logger, config.cacheDir, cacheKey);
|
|
403
|
+
if (disk) {
|
|
404
|
+
logger.debug('Reusing on-disk TS schemas (types unchanged across runs)');
|
|
405
|
+
cachedCustomTypesContent = customTypesContent;
|
|
406
|
+
cachedTSSchemas = disk;
|
|
407
|
+
return { ...disk, ...zodSchemas };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
337
410
|
const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, zodSchemas);
|
|
338
411
|
cachedCustomTypesContent = customTypesContent;
|
|
339
412
|
cachedTSSchemas = tsSchemas;
|
|
413
|
+
if (config.cacheDir && cacheKey) {
|
|
414
|
+
writeDiskTSSchemas(logger, config.cacheDir, cacheKey, tsSchemas);
|
|
415
|
+
}
|
|
340
416
|
return { ...tsSchemas, ...zodSchemas };
|
|
341
417
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WorkflowStepMeta, WorkflowPlannedStep } from '@pikku/core/workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Derive the static UI plan for a DSL workflow from its extracted step tree.
|
|
4
|
+
*
|
|
5
|
+
* `plannedSteps` is the ordered list of every named step the workflow can run,
|
|
6
|
+
* so a frontend can render the step skeleton up front without executing the
|
|
7
|
+
* workflow or hand-listing steps. It is populated whenever the workflow has NO
|
|
8
|
+
* loops (fanout) — loops make the step COUNT runtime-dependent, so a workflow
|
|
9
|
+
* containing any fanout gets neither field.
|
|
10
|
+
*
|
|
11
|
+
* `deterministic` is true only when the exact executed sequence is known up
|
|
12
|
+
* front: a flat list of named steps with no branches/switches (which pick a
|
|
13
|
+
* path at runtime) and no loops. A branchy-but-loopless workflow is therefore
|
|
14
|
+
* `deterministic: false` but still gets `plannedSteps` (the full set of
|
|
15
|
+
* possible steps, in source order).
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveWorkflowPlan(steps: WorkflowStepMeta[]): {
|
|
18
|
+
deterministic: boolean;
|
|
19
|
+
plannedSteps?: WorkflowPlannedStep[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the static UI plan for a DSL workflow from its extracted step tree.
|
|
3
|
+
*
|
|
4
|
+
* `plannedSteps` is the ordered list of every named step the workflow can run,
|
|
5
|
+
* so a frontend can render the step skeleton up front without executing the
|
|
6
|
+
* workflow or hand-listing steps. It is populated whenever the workflow has NO
|
|
7
|
+
* loops (fanout) — loops make the step COUNT runtime-dependent, so a workflow
|
|
8
|
+
* containing any fanout gets neither field.
|
|
9
|
+
*
|
|
10
|
+
* `deterministic` is true only when the exact executed sequence is known up
|
|
11
|
+
* front: a flat list of named steps with no branches/switches (which pick a
|
|
12
|
+
* path at runtime) and no loops. A branchy-but-loopless workflow is therefore
|
|
13
|
+
* `deterministic: false` but still gets `plannedSteps` (the full set of
|
|
14
|
+
* possible steps, in source order).
|
|
15
|
+
*/
|
|
16
|
+
export function deriveWorkflowPlan(steps) {
|
|
17
|
+
if (containsLoop(steps)) {
|
|
18
|
+
return { deterministic: false };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
deterministic: !containsConditional(steps),
|
|
22
|
+
plannedSteps: collectNamedSteps(steps),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Any fanout (loop) anywhere in the tree — including inside branches/switches. */
|
|
26
|
+
function containsLoop(steps) {
|
|
27
|
+
return steps.some((step) => {
|
|
28
|
+
switch (step.type) {
|
|
29
|
+
case 'fanout':
|
|
30
|
+
return true;
|
|
31
|
+
case 'branch':
|
|
32
|
+
return (step.branches.some((b) => containsLoop(b.steps)) ||
|
|
33
|
+
(step.elseSteps ? containsLoop(step.elseSteps) : false));
|
|
34
|
+
case 'switch':
|
|
35
|
+
return (step.cases.some((c) => containsLoop(c.steps)) ||
|
|
36
|
+
(step.defaultSteps ? containsLoop(step.defaultSteps) : false));
|
|
37
|
+
default:
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Any branch/switch — the run takes a runtime-decided path. */
|
|
43
|
+
function containsConditional(steps) {
|
|
44
|
+
return steps.some((step) => step.type === 'branch' || step.type === 'switch');
|
|
45
|
+
}
|
|
46
|
+
/** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
|
|
47
|
+
function collectNamedSteps(steps) {
|
|
48
|
+
const planned = [];
|
|
49
|
+
for (const step of steps) {
|
|
50
|
+
switch (step.type) {
|
|
51
|
+
case 'rpc':
|
|
52
|
+
case 'inline':
|
|
53
|
+
case 'sleep':
|
|
54
|
+
planned.push({ stepName: step.stepName });
|
|
55
|
+
break;
|
|
56
|
+
case 'parallel':
|
|
57
|
+
for (const child of step.children) {
|
|
58
|
+
planned.push({ stepName: child.stepName });
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case 'branch':
|
|
62
|
+
for (const b of step.branches)
|
|
63
|
+
planned.push(...collectNamedSteps(b.steps));
|
|
64
|
+
if (step.elseSteps)
|
|
65
|
+
planned.push(...collectNamedSteps(step.elseSteps));
|
|
66
|
+
break;
|
|
67
|
+
case 'switch':
|
|
68
|
+
for (const c of step.cases)
|
|
69
|
+
planned.push(...collectNamedSteps(c.steps));
|
|
70
|
+
if (step.defaultSteps) {
|
|
71
|
+
planned.push(...collectNamedSteps(step.defaultSteps));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
// set / return / cancel / filter / arrayPredicate produce no named step
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return planned;
|
|
78
|
+
}
|