@pikku/inspector 0.12.9 → 0.12.11
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 +26 -9
- package/dist/add/add-functions.js +74 -4
- package/dist/add/add-http-route.js +20 -1
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +10 -10
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/utils/extract-node-value.js +4 -0
- package/dist/utils/schema-generator.js +3 -3
- package/dist/utils/validate-auth-sessionless.d.ts +1 -1
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +5 -4
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +47 -28
- package/dist/utils/workflow/dsl/patterns.d.ts +5 -1
- package/dist/utils/workflow/dsl/patterns.js +2 -2
- package/dist/utils/workflow/dsl/validation.d.ts +4 -2
- package/dist/utils/workflow/dsl/validation.js +16 -8
- package/package.json +2 -2
- package/src/add/add-functions.test.ts +169 -0
- package/src/add/add-functions.ts +135 -8
- package/src/add/add-http-route.ts +28 -1
- package/src/add/add-workflow.ts +11 -11
- package/src/error-codes.ts +1 -0
- package/src/utils/extract-node-value.ts +5 -0
- package/src/utils/schema-generator.ts +3 -3
- package/src/utils/validate-auth-sessionless.ts +1 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +58 -32
- package/src/utils/workflow/dsl/patterns.ts +2 -2
- package/src/utils/workflow/dsl/validation.ts +21 -9
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
## 0.12.11
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 033d172: Log a critical inspector error when multiple functions resolve to the same `pikku` function name, instead of silently allowing routing map collisions. This may cause builds to fail if multiple functions previously resolved to the same `pikku` function name.
|
|
6
|
+
- Updated dependencies [b9ed73e]
|
|
7
|
+
- @pikku/core@0.12.19
|
|
8
|
+
|
|
1
9
|
## 0.12.0
|
|
2
10
|
|
|
11
|
+
## 0.12.10
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- ba8d6ff: Support inline functions in pikkuWorkflowComplexFunc with full DSL extraction
|
|
16
|
+
- d3ace0e: Inspector now captures the `deploy: 'serverless' | 'server' | 'auto'` option
|
|
17
|
+
from `pikkuFunc` / `pikkuSessionlessFunc` calls, alongside the other runtime
|
|
18
|
+
metadata (`expose`, `remote`, `mcp`, `readonly`, `approvalRequired`).
|
|
19
|
+
|
|
20
|
+
Previously this field was defined on `FunctionRuntimeMeta` but never read
|
|
21
|
+
from the user's source, so `deploy: 'server'` was silently dropped. That
|
|
22
|
+
left downstream consumers — notably `@pikku/cli`'s deployment analyzer,
|
|
23
|
+
which routes server-targeted functions to a container unit — treating
|
|
24
|
+
every function as `serverless` regardless of its declared intent.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [311c0c4]
|
|
27
|
+
- @pikku/core@0.12.18
|
|
28
|
+
|
|
3
29
|
## 0.12.9
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
|
@@ -14,7 +40,6 @@
|
|
|
14
40
|
### Patch Changes
|
|
15
41
|
|
|
16
42
|
- 624097e: Add deploy pipeline with provider-agnostic architecture
|
|
17
|
-
|
|
18
43
|
- Add MetaService with explicit typed API, absorb WiringService reads
|
|
19
44
|
- Add deployment service, traceId propagation, scoped logger
|
|
20
45
|
- Rewrite analyzer: one function = one worker, gateways dispatch via RPC
|
|
@@ -61,7 +86,6 @@
|
|
|
61
86
|
|
|
62
87
|
- 5866b66: Add critical error (PKU490) when Zod schemas and wiring calls (wireHTTPRoutes, addPermission, addHTTPMiddleware) coexist in the same file. The CLI uses tsImport to extract Zod schemas at runtime, which executes all top-level code — wiring side-effects crash in this context because pikku state metadata doesn't exist. Schemas and wirings must be in separate files.
|
|
63
88
|
- e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
|
|
64
|
-
|
|
65
89
|
- Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
|
|
66
90
|
- Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
|
|
67
91
|
- Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
|
|
@@ -190,14 +214,12 @@
|
|
|
190
214
|
- 1967172: Update code generation to support channel middleware enhancements
|
|
191
215
|
|
|
192
216
|
**Code Generation Updates:**
|
|
193
|
-
|
|
194
217
|
- Update channel type serialization to include middleware support
|
|
195
218
|
- Improve WebSocket wrapper generation for middleware handling
|
|
196
219
|
- Update CLI channel client generation with better type support
|
|
197
220
|
- Enhance services and schema generation for channel configurations
|
|
198
221
|
|
|
199
222
|
**Inspector Updates:**
|
|
200
|
-
|
|
201
223
|
- Improve channel metadata extraction for middleware
|
|
202
224
|
- Better type analysis for channel lifecycle functions
|
|
203
225
|
- Enhanced post-processing for channel configurations
|
|
@@ -205,19 +227,16 @@
|
|
|
205
227
|
- 753481a: Add bootstrap command, performance optimizations, and CLI improvements
|
|
206
228
|
|
|
207
229
|
**New Features:**
|
|
208
|
-
|
|
209
230
|
- Add `pikku bootstrap` command for type-only generation (~13.5% faster than `pikku all`)
|
|
210
231
|
- Add configurable `ignoreFiles` option to pikku.config.json with sensible defaults (_.gen.ts, _.test.ts, \*.spec.ts)
|
|
211
232
|
- Export pikkuCLIRender helper from serialize-cli-types.ts with JSDoc documentation
|
|
212
233
|
|
|
213
234
|
**Performance Improvements:**
|
|
214
|
-
|
|
215
235
|
- Add aggressive TypeScript compiler options (skipDefaultLibCheck, types: []) - ~37% faster TypeScript setup
|
|
216
236
|
- Add detailed performance timing to inspector phases (--logLevel=debug)
|
|
217
237
|
- Optimize file inspection with ignore patterns - ~10-20% faster overall
|
|
218
238
|
|
|
219
239
|
**Enhancements:**
|
|
220
|
-
|
|
221
240
|
- Fix --logLevel flag to properly apply log level to logger
|
|
222
241
|
- Update middleware logging to use structured log format
|
|
223
242
|
- Improve CLI renderers to consistently use destructured logger service
|
|
@@ -318,7 +337,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
|
318
337
|
### Patch Changes
|
|
319
338
|
|
|
320
339
|
- 44e3ff4: feat: enhance CLI filtering with type and directory filters
|
|
321
|
-
|
|
322
340
|
- Add --types filter to filter by PikkuEventTypes (http, channel, queue, scheduler, rpc, mcp)
|
|
323
341
|
- Add --directories filter to filter by file paths/directories
|
|
324
342
|
- All filters (tags, types, directories) now work together with AND logic
|
|
@@ -329,7 +347,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
|
329
347
|
- 7c592b8: feat: support for required services and improved service configuration
|
|
330
348
|
|
|
331
349
|
This release includes several enhancements to service management and configuration:
|
|
332
|
-
|
|
333
350
|
- Added support for required services configuration
|
|
334
351
|
- Improved service discovery and registration
|
|
335
352
|
- Added typed RPC clients for service communication
|
|
@@ -3,7 +3,7 @@ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
|
3
3
|
import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
|
|
4
4
|
import { extractFunctionNode } from '../utils/extract-function-node.js';
|
|
5
5
|
import { extractUsedWires } from '../utils/extract-services.js';
|
|
6
|
-
import { formatVersionedId } from '@pikku/core';
|
|
6
|
+
import { formatVersionedId, parseVersionedId } from '@pikku/core';
|
|
7
7
|
import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
|
|
8
8
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
9
9
|
import { resolvePermissions } from '../utils/permissions.js';
|
|
@@ -209,6 +209,19 @@ function unwrapPromise(checker, type) {
|
|
|
209
209
|
}
|
|
210
210
|
return type;
|
|
211
211
|
}
|
|
212
|
+
const resolveExistingFunctionSource = (state, pikkuFuncId) => {
|
|
213
|
+
return (state.functions.meta[pikkuFuncId]?.sourceFile ||
|
|
214
|
+
state.rpc.internalFiles.get(pikkuFuncId)?.path ||
|
|
215
|
+
null);
|
|
216
|
+
};
|
|
217
|
+
const areCompatibleFunctionIds = (existingId, incomingId) => {
|
|
218
|
+
if (existingId === incomingId) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const existingParsed = parseVersionedId(existingId);
|
|
222
|
+
const incomingParsed = parseVersionedId(incomingId);
|
|
223
|
+
return existingParsed.baseName === incomingParsed.baseName;
|
|
224
|
+
};
|
|
212
225
|
/**
|
|
213
226
|
* Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
|
|
214
227
|
* then push into state.functions.meta.
|
|
@@ -245,6 +258,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
245
258
|
let remote;
|
|
246
259
|
let mcp;
|
|
247
260
|
let readonly_;
|
|
261
|
+
let deploy;
|
|
248
262
|
let approvalRequired;
|
|
249
263
|
let approvalDescription;
|
|
250
264
|
let version;
|
|
@@ -312,6 +326,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
312
326
|
remote = getPropertyValue(firstArg, 'remote');
|
|
313
327
|
mcp = getPropertyValue(firstArg, 'mcp');
|
|
314
328
|
readonly_ = getPropertyValue(firstArg, 'readonly');
|
|
329
|
+
deploy = getPropertyValue(firstArg, 'deploy');
|
|
315
330
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
|
|
316
331
|
// Extract approvalDescription identifier reference
|
|
317
332
|
for (const prop of firstArg.properties) {
|
|
@@ -398,6 +413,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
398
413
|
pikkuFuncId = formatVersionedId(baseName, version);
|
|
399
414
|
}
|
|
400
415
|
const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc';
|
|
416
|
+
const isListFunc = expression.text === 'pikkuListFunc';
|
|
401
417
|
const mcpEnabled = mcp || isMCPToolFunc;
|
|
402
418
|
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
403
419
|
const handler = resolvedFunc &&
|
|
@@ -459,7 +475,22 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
459
475
|
state.schemaLookup.set(schemaName, inputSchemaRef);
|
|
460
476
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
|
|
461
477
|
}
|
|
462
|
-
else if (genericTypes.length >= 1 && genericTypes[0]) {
|
|
478
|
+
else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
479
|
+
const inputAliasName = `${capitalizedName}Input`;
|
|
480
|
+
const filterType = genericTypes[0];
|
|
481
|
+
const filterTypeText = checker.typeToString(filterType, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
482
|
+
const refs = resolveTypeImports(filterType, state.functions.typesMap, true, checker);
|
|
483
|
+
state.functions.typesMap.addCustomType(inputAliasName, `{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`, [...new Set(refs)]);
|
|
484
|
+
inputNames = [inputAliasName];
|
|
485
|
+
const secondParam = handler.parameters[1];
|
|
486
|
+
if (secondParam) {
|
|
487
|
+
inputTypes = [checker.getTypeAtLocation(secondParam)];
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
inputTypes = [filterType];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
463
494
|
// Fall back to extracting from generic type arguments
|
|
464
495
|
const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
|
|
465
496
|
inputNames = result.names;
|
|
@@ -483,7 +514,15 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
483
514
|
state.schemaLookup.set(schemaName, outputSchemaRef);
|
|
484
515
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
|
|
485
516
|
}
|
|
486
|
-
else if (genericTypes.length >= 2) {
|
|
517
|
+
else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
|
|
518
|
+
const outputAliasName = `${capitalizedName}Output`;
|
|
519
|
+
const rowType = genericTypes[1];
|
|
520
|
+
const rowTypeText = checker.typeToString(rowType, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
521
|
+
const refs = resolveTypeImports(rowType, state.functions.typesMap, true, checker);
|
|
522
|
+
state.functions.typesMap.addCustomType(outputAliasName, `{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`, [...new Set(refs)]);
|
|
523
|
+
outputNames = [outputAliasName];
|
|
524
|
+
}
|
|
525
|
+
else if (!isListFunc && genericTypes.length >= 2) {
|
|
487
526
|
outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', name, genericTypes[1]).names;
|
|
488
527
|
}
|
|
489
528
|
else {
|
|
@@ -537,6 +576,36 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
537
576
|
if (inputTypes.length > 0) {
|
|
538
577
|
state.typesLookup.set(pikkuFuncId, inputTypes);
|
|
539
578
|
}
|
|
579
|
+
const sourceFile = node.getSourceFile().fileName;
|
|
580
|
+
const existingFunction = state.functions.meta[pikkuFuncId];
|
|
581
|
+
if (existingFunction &&
|
|
582
|
+
existingFunction.sourceFile &&
|
|
583
|
+
existingFunction.sourceFile !== sourceFile) {
|
|
584
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
|
|
585
|
+
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (exportedName || explicitName) {
|
|
589
|
+
const existingInternal = state.rpc.internalMeta[name];
|
|
590
|
+
if (existingInternal &&
|
|
591
|
+
!areCompatibleFunctionIds(existingInternal, pikkuFuncId)) {
|
|
592
|
+
const existingSource = resolveExistingFunctionSource(state, existingInternal) || 'unknown file';
|
|
593
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
|
|
594
|
+
`It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (expose) {
|
|
598
|
+
const existingExposed = state.rpc.exposedMeta[name];
|
|
599
|
+
if (existingExposed &&
|
|
600
|
+
!areCompatibleFunctionIds(existingExposed, pikkuFuncId)) {
|
|
601
|
+
const existingSource = resolveExistingFunctionSource(state, existingExposed) ||
|
|
602
|
+
'unknown file';
|
|
603
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Exposed function name '${name}' is not unique. ` +
|
|
604
|
+
`It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
540
609
|
// --- resolve middleware ---
|
|
541
610
|
let middleware = objectNode
|
|
542
611
|
? resolveMiddleware(state, objectNode, tags, checker)
|
|
@@ -583,6 +652,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
583
652
|
remote: remote || undefined,
|
|
584
653
|
mcp: mcpEnabled || undefined,
|
|
585
654
|
readonly: readonly_ || undefined,
|
|
655
|
+
deploy: deploy || undefined,
|
|
586
656
|
approvalRequired: approvalRequired || undefined,
|
|
587
657
|
approvalDescription: approvalDescription || undefined,
|
|
588
658
|
version,
|
|
@@ -594,7 +664,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
594
664
|
middleware,
|
|
595
665
|
permissions,
|
|
596
666
|
isDirectFunction,
|
|
597
|
-
sourceFile
|
|
667
|
+
sourceFile,
|
|
598
668
|
exportedName: exportedName || undefined,
|
|
599
669
|
};
|
|
600
670
|
// Populate node metadata if node config is present
|
|
@@ -7,6 +7,7 @@ import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js';
|
|
|
7
7
|
import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js';
|
|
8
8
|
import { extractWireNames } from '../utils/post-process.js';
|
|
9
9
|
import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js';
|
|
10
|
+
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
|
|
10
11
|
import { ErrorCode } from '../error-codes.js';
|
|
11
12
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
|
|
12
13
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
@@ -113,12 +114,30 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
113
114
|
if (funcName.startsWith('__temp_')) {
|
|
114
115
|
funcName = makeContextBasedId('http', method, fullRoute);
|
|
115
116
|
}
|
|
117
|
+
let refAddonTarget = null;
|
|
118
|
+
if (ts.isCallExpression(funcInitializer) &&
|
|
119
|
+
ts.isIdentifier(funcInitializer.expression) &&
|
|
120
|
+
funcInitializer.expression.text === 'ref') {
|
|
121
|
+
const [firstArg] = funcInitializer.arguments;
|
|
122
|
+
if (firstArg &&
|
|
123
|
+
ts.isStringLiteral(firstArg) &&
|
|
124
|
+
firstArg.text.includes(':')) {
|
|
125
|
+
refAddonTarget = firstArg.text;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
117
129
|
? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
|
|
118
130
|
: null;
|
|
131
|
+
if (refAddonTarget) {
|
|
132
|
+
const targetMeta = resolveFunctionMeta(state, refAddonTarget);
|
|
133
|
+
if (!targetMeta) {
|
|
134
|
+
logger.warn(`Skipping route '${fullRoute}': addon function metadata for '${refAddonTarget}' is not available yet.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
|
|
120
139
|
// Lookup existing function metadata
|
|
121
|
-
const fnMeta = state
|
|
140
|
+
const fnMeta = resolveFunctionMeta(state, funcName);
|
|
122
141
|
if (!fnMeta) {
|
|
123
142
|
logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for '${funcName}'.`);
|
|
124
143
|
return;
|
|
@@ -5,7 +5,7 @@ import type { WorkflowStepMeta } from '@pikku/core/workflow';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
|
|
7
7
|
/**
|
|
8
|
-
* Inspector for
|
|
8
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
9
9
|
* Detects workflow registration and extracts metadata
|
|
10
10
|
*/
|
|
11
11
|
export declare const addWorkflow: AddWiring;
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -142,7 +142,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
|
-
* Inspector for
|
|
145
|
+
* Inspector for pikkuWorkflowFunc() and pikkuWorkflowComplexFunc() calls
|
|
146
146
|
* Detects workflow registration and extracts metadata
|
|
147
147
|
*/
|
|
148
148
|
export const addWorkflow = (logger, node, checker, state) => {
|
|
@@ -160,7 +160,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
160
160
|
wrapperType = 'dsl';
|
|
161
161
|
}
|
|
162
162
|
else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
163
|
-
wrapperType = '
|
|
163
|
+
wrapperType = 'complex';
|
|
164
164
|
}
|
|
165
165
|
else {
|
|
166
166
|
return;
|
|
@@ -220,7 +220,9 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
220
220
|
let dsl = undefined;
|
|
221
221
|
// Try DSL workflow extraction first
|
|
222
222
|
// Pass the whole CallExpression node so findWorkflowFunction can find the arrow function
|
|
223
|
-
const result = extractDSLWorkflow(node, checker
|
|
223
|
+
const result = extractDSLWorkflow(node, checker, {
|
|
224
|
+
allowInline: wrapperType === 'complex',
|
|
225
|
+
});
|
|
224
226
|
if (result.status === 'ok' && result.steps) {
|
|
225
227
|
// Extraction succeeded
|
|
226
228
|
steps = result.steps;
|
|
@@ -252,7 +254,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
252
254
|
// For pikkuWorkflowFunc, this is a critical error
|
|
253
255
|
// But still track RPC invocations for function registration
|
|
254
256
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
|
|
255
|
-
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}'
|
|
257
|
+
logger.critical(ErrorCode.INVALID_DSL_WORKFLOW, `Workflow '${workflowName}' does not conform to DSL workflow rules:\n${result.reason || 'Unknown error'}`);
|
|
256
258
|
return;
|
|
257
259
|
}
|
|
258
260
|
else {
|
|
@@ -261,12 +263,10 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
261
263
|
dsl = false;
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*/
|
|
269
|
-
if (!dsl || wrapperType === 'regular') {
|
|
266
|
+
// For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
|
|
267
|
+
// patterns the DSL extractor doesn't handle (array+push, nested Promise.all
|
|
268
|
+
// with identifier args, etc.) are still registered as invoked functions.
|
|
269
|
+
if (wrapperType === 'complex') {
|
|
270
270
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
|
|
271
271
|
}
|
|
272
272
|
state.workflows.meta[workflowName] = {
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare enum ErrorCode {
|
|
|
41
41
|
PERMISSION_EMPTY_ARRAY = "PKU937",
|
|
42
42
|
PERMISSION_PATTERN_INVALID = "PKU975",
|
|
43
43
|
DUPLICATE_FUNCTION_VERSION = "PKU850",
|
|
44
|
+
DUPLICATE_FUNCTION_NAME = "PKU851",
|
|
44
45
|
MANIFEST_MISSING = "PKU860",
|
|
45
46
|
FUNCTION_VERSION_MODIFIED = "PKU861",
|
|
46
47
|
CONTRACT_CHANGED_REQUIRES_BUMP = "PKU862",
|
package/dist/error-codes.js
CHANGED
|
@@ -48,6 +48,7 @@ export var ErrorCode;
|
|
|
48
48
|
ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
|
|
49
49
|
// Versioning errors
|
|
50
50
|
ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
|
|
51
|
+
ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
|
|
51
52
|
// Contract versioning errors
|
|
52
53
|
ErrorCode["MANIFEST_MISSING"] = "PKU860";
|
|
53
54
|
ErrorCode["FUNCTION_VERSION_MODIFIED"] = "PKU861";
|
|
@@ -20,6 +20,10 @@ export function extractStringLiteral(node, checker) {
|
|
|
20
20
|
}
|
|
21
21
|
return result;
|
|
22
22
|
}
|
|
23
|
+
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
24
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
25
|
+
return extractStringLiteral(node.expression, checker);
|
|
26
|
+
}
|
|
23
27
|
// Try to evaluate constant identifiers
|
|
24
28
|
if (ts.isIdentifier(node)) {
|
|
25
29
|
const symbol = checker.getSymbolAtLocation(node);
|
|
@@ -258,10 +258,10 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
258
258
|
const uniqueSourceFiles = [
|
|
259
259
|
...new Set([...schemaLookup.values()].map((ref) => ref.sourceFile)),
|
|
260
260
|
];
|
|
261
|
-
|
|
261
|
+
logger.info(`[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`);
|
|
262
262
|
const importStart = performance.now();
|
|
263
263
|
const importedModules = await batchImportWithRegister(logger, uniqueSourceFiles);
|
|
264
|
-
|
|
264
|
+
logger.info(`[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`);
|
|
265
265
|
const processStart = performance.now();
|
|
266
266
|
// Track schemas that need per-file tsImport fallback
|
|
267
267
|
const fallbackSchemas = [];
|
|
@@ -302,7 +302,7 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
logger.info(`[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`);
|
|
306
306
|
return schemas;
|
|
307
307
|
}
|
|
308
308
|
export async function generateAllSchemas(logger, config, state) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
2
|
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
3
3
|
export declare function validateAuthSessionless(logger: InspectorLogger, obj: ts.ObjectLiteralExpression, state: InspectorState, funcName: string, wireDescription: string, inheritedAuth?: boolean): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import type { WorkflowStepMeta, WorkflowContext } from '@pikku/core/workflow';
|
|
3
3
|
/**
|
|
4
|
-
* Result of
|
|
4
|
+
* Result of DSL workflow extraction
|
|
5
5
|
*/
|
|
6
6
|
export interface ExtractionResult {
|
|
7
7
|
status: 'ok' | 'error';
|
|
@@ -9,9 +9,10 @@ export interface ExtractionResult {
|
|
|
9
9
|
/** Workflow context (top-level variables) */
|
|
10
10
|
context?: WorkflowContext;
|
|
11
11
|
reason?: string;
|
|
12
|
-
simple?: boolean;
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
|
-
* Extract
|
|
14
|
+
* Extract DSL workflow metadata from a function declaration
|
|
16
15
|
*/
|
|
17
|
-
export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker
|
|
16
|
+
export declare function extractDSLWorkflow(funcNode: ts.Node, checker: ts.TypeChecker, options?: {
|
|
17
|
+
allowInline?: boolean;
|
|
18
|
+
}): ExtractionResult;
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowSleepCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowSleepCall, 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
|
/**
|
|
6
|
-
* Extract
|
|
6
|
+
* Extract DSL workflow metadata from a function declaration
|
|
7
7
|
*/
|
|
8
|
-
function
|
|
9
|
-
if (ts.isIdentifier(expr)) {
|
|
10
|
-
return expr.text;
|
|
11
|
-
}
|
|
12
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
13
|
-
const base = extractSourcePath(expr.expression);
|
|
14
|
-
if (base) {
|
|
15
|
-
return `${base}.${expr.name.text}`;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Extract simple workflow metadata from a function declaration
|
|
22
|
-
*/
|
|
23
|
-
export function extractDSLWorkflow(funcNode, checker) {
|
|
8
|
+
export function extractDSLWorkflow(funcNode, checker, options) {
|
|
24
9
|
try {
|
|
25
10
|
// Find the async arrow function
|
|
26
11
|
const arrowFunc = findWorkflowFunction(funcNode);
|
|
@@ -28,7 +13,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
28
13
|
return {
|
|
29
14
|
status: 'error',
|
|
30
15
|
reason: 'Could not find async arrow function in workflow definition',
|
|
31
|
-
simple: false,
|
|
32
16
|
};
|
|
33
17
|
}
|
|
34
18
|
// Extract input parameter name (second parameter)
|
|
@@ -37,7 +21,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
37
21
|
return {
|
|
38
22
|
status: 'error',
|
|
39
23
|
reason: 'Could not determine input parameter name',
|
|
40
|
-
simple: false,
|
|
41
24
|
};
|
|
42
25
|
}
|
|
43
26
|
// Initialize extraction context
|
|
@@ -53,12 +36,13 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
53
36
|
depth: 0,
|
|
54
37
|
};
|
|
55
38
|
// Validate no disallowed patterns
|
|
56
|
-
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body
|
|
39
|
+
const patternErrors = validateNoDisallowedPatterns(arrowFunc.body, {
|
|
40
|
+
allowInline: options?.allowInline,
|
|
41
|
+
});
|
|
57
42
|
if (patternErrors.length > 0) {
|
|
58
43
|
return {
|
|
59
44
|
status: 'error',
|
|
60
45
|
reason: formatValidationErrors(patternErrors),
|
|
61
|
-
simple: false,
|
|
62
46
|
};
|
|
63
47
|
}
|
|
64
48
|
// Validate all workflow calls are awaited
|
|
@@ -67,7 +51,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
67
51
|
return {
|
|
68
52
|
status: 'error',
|
|
69
53
|
reason: formatValidationErrors(awaitErrors),
|
|
70
|
-
simple: false,
|
|
71
54
|
};
|
|
72
55
|
}
|
|
73
56
|
// Extract steps from function body
|
|
@@ -77,7 +60,6 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
77
60
|
return {
|
|
78
61
|
status: 'error',
|
|
79
62
|
reason: formatValidationErrors(context.errors),
|
|
80
|
-
simple: false,
|
|
81
63
|
};
|
|
82
64
|
}
|
|
83
65
|
// Build workflow context from extracted context variables
|
|
@@ -92,14 +74,12 @@ export function extractDSLWorkflow(funcNode, checker) {
|
|
|
92
74
|
status: 'ok',
|
|
93
75
|
steps,
|
|
94
76
|
context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
|
|
95
|
-
simple: true,
|
|
96
77
|
};
|
|
97
78
|
}
|
|
98
79
|
catch (error) {
|
|
99
80
|
return {
|
|
100
81
|
status: 'error',
|
|
101
82
|
reason: error instanceof Error ? error.message : String(error),
|
|
102
|
-
simple: false,
|
|
103
83
|
};
|
|
104
84
|
}
|
|
105
85
|
}
|
|
@@ -248,7 +228,9 @@ function extractVariableDeclaration(statement, context) {
|
|
|
248
228
|
if (ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)) {
|
|
249
229
|
const call = init.expression;
|
|
250
230
|
if (isWorkflowDoCall(call, context.checker)) {
|
|
251
|
-
const step =
|
|
231
|
+
const step = isInlineDoCall(call)
|
|
232
|
+
? extractInlineStep(call, context)
|
|
233
|
+
: extractRpcStep(call, context, varName);
|
|
252
234
|
if (step) {
|
|
253
235
|
// Track output variable
|
|
254
236
|
const type = context.checker.getTypeAtLocation(decl);
|
|
@@ -335,7 +317,9 @@ function extractExpressionStatement(statement, context) {
|
|
|
335
317
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
336
318
|
const call = expr.expression;
|
|
337
319
|
if (isWorkflowDoCall(call, context.checker)) {
|
|
338
|
-
const step =
|
|
320
|
+
const step = isInlineDoCall(call)
|
|
321
|
+
? extractInlineStep(call, context)
|
|
322
|
+
: extractRpcStep(call, context, outputVar);
|
|
339
323
|
// Track output variable if this is an assignment
|
|
340
324
|
if (outputVar && step) {
|
|
341
325
|
const type = context.checker.getTypeAtLocation(expr);
|
|
@@ -394,6 +378,41 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
394
378
|
return null;
|
|
395
379
|
}
|
|
396
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Extract inline step from workflow.do() call with a function argument
|
|
383
|
+
*/
|
|
384
|
+
function extractInlineStep(call, context) {
|
|
385
|
+
const args = call.arguments;
|
|
386
|
+
if (args.length < 2)
|
|
387
|
+
return null;
|
|
388
|
+
try {
|
|
389
|
+
const stepName = extractStringLiteral(args[0], context.checker);
|
|
390
|
+
const optionsArg = args.length >= 3 ? args[args.length - 1] : undefined;
|
|
391
|
+
const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
392
|
+
? extractStepOptions(optionsArg, context)
|
|
393
|
+
: undefined;
|
|
394
|
+
return {
|
|
395
|
+
type: 'inline',
|
|
396
|
+
stepName,
|
|
397
|
+
options,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
context.errors.push({
|
|
402
|
+
message: `Failed to extract inline step: ${error instanceof Error ? error.message : String(error)}`,
|
|
403
|
+
node: call,
|
|
404
|
+
});
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if the second argument of a workflow.do() call is a function
|
|
410
|
+
*/
|
|
411
|
+
function isInlineDoCall(call) {
|
|
412
|
+
const secondArg = call.arguments[1];
|
|
413
|
+
return (!!secondArg &&
|
|
414
|
+
(ts.isArrowFunction(secondArg) || ts.isFunctionExpression(secondArg)));
|
|
415
|
+
}
|
|
397
416
|
/**
|
|
398
417
|
* Extract step options from options object
|
|
399
418
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Pattern detection helpers for
|
|
3
|
+
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do()
|
|
@@ -43,6 +43,10 @@ export declare function isParallelGroup(node: ts.CallExpression): boolean;
|
|
|
43
43
|
* Check if a for statement is a valid sequential fanout (for..of)
|
|
44
44
|
*/
|
|
45
45
|
export declare function isSequentialFanout(node: ts.ForOfStatement): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Extract full source path from an expression (e.g., data.memberEmails)
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractSourcePath(expr: ts.Expression): string | null;
|
|
46
50
|
/**
|
|
47
51
|
* Extract the variable name from a for..of statement
|
|
48
52
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Pattern detection helpers for
|
|
3
|
+
* Pattern detection helpers for DSL workflow extraction
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do()
|
|
@@ -162,7 +162,7 @@ export function isSequentialFanout(node) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Extract full source path from an expression (e.g., data.memberEmails)
|
|
164
164
|
*/
|
|
165
|
-
function extractSourcePath(expr) {
|
|
165
|
+
export function extractSourcePath(expr) {
|
|
166
166
|
if (ts.isIdentifier(expr)) {
|
|
167
167
|
return expr.text;
|
|
168
168
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
|
-
* Validation rules for
|
|
3
|
+
* Validation rules for DSL workflows
|
|
4
4
|
*/
|
|
5
5
|
export interface ValidationError {
|
|
6
6
|
message: string;
|
|
@@ -19,7 +19,9 @@ export interface ValidationError {
|
|
|
19
19
|
* - ThrowStatement (for WorkflowCancelledException)
|
|
20
20
|
* - Block (containers)
|
|
21
21
|
*/
|
|
22
|
-
export declare function validateNoDisallowedPatterns(node: ts.Node
|
|
22
|
+
export declare function validateNoDisallowedPatterns(node: ts.Node, options?: {
|
|
23
|
+
allowInline?: boolean;
|
|
24
|
+
}): ValidationError[];
|
|
23
25
|
/**
|
|
24
26
|
* Validate that all workflow.do calls are awaited
|
|
25
27
|
*/
|