@pikku/inspector 0.12.89 → 0.12.92
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 +42 -0
- package/dist/add/add-analytics.d.ts +23 -0
- package/dist/add/add-analytics.js +7 -8
- package/dist/add/add-outgoing-webhook.d.ts +3 -0
- package/dist/add/add-outgoing-webhook.js +81 -0
- package/dist/add/add-trigger.d.ts +5 -0
- package/dist/add/add-trigger.js +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +3 -0
- package/dist/types.d.ts +10 -1
- package/dist/utils/addon-resolution.d.ts +10 -0
- package/dist/utils/addon-resolution.js +43 -0
- package/dist/utils/find-zod-transform.d.ts +3 -3
- package/dist/utils/find-zod-transform.js +3 -3
- package/dist/utils/load-addon-functions-meta.js +1 -40
- package/dist/utils/post-process.js +58 -36
- package/dist/utils/resolve-versions.js +6 -0
- package/dist/utils/schema-generator.js +5 -10
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/zod-to-type-text.d.ts +25 -0
- package/dist/utils/zod-to-type-text.js +228 -0
- package/dist/visit.js +2 -0
- package/package.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## 0.12.92
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- f84e173: An addon wired from a workspace package now resolves from that package. The installed-addon check (PKU340), the remote addon `devDependencies` check and `pikku db generate`'s addon schemas all looked only at the project root, so an addon declared where it is wired — the only place bun links it — failed codegen unless it was also declared at the root.
|
|
6
|
+
- Updated dependencies [f02585e]
|
|
7
|
+
- Updated dependencies [5586749]
|
|
8
|
+
- @pikku/core@0.12.127
|
|
9
|
+
|
|
10
|
+
## 0.12.91
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 5e93f30: Add `defineOutgoingWebhook({ event, title, description?, payload })` in `@pikku/core/webhook`. The CLI collects every exported declaration into `.pikku/webhooks/pikku-outgoing-webhooks-meta.gen.json` and `pikku-outgoing-webhooks.gen.ts`, which exports `OutgoingWebhooksMap`, `TypedWebhookService` and `typedWebhookService(service)`: `send` checks `data` against the declared payload for a declared event and accepts any other event unchanged. `MetaService.getOutgoingWebhooksMeta()` and the console addon's `outgoingWebhooksMeta` serve the declarations.
|
|
15
|
+
- 5e93f30: Add `wireTriggerWebhookSource({ name, method?, route?, secret?, events, receive?, check?, setup?, teardown? })` in `#pikku/trigger`. Each source becomes a `POST /webhooks/<name>` route whose events are validated against their schemas and queued on `pikku-incoming-webhooks` through `IncomingWebhookService`; a generated worker runs the matching `wireTrigger({ name: '<source>:<event>' })` and the queue retries it on failure. `pikku webhooks status | setup | teardown --url --labelPrefix [--previous]` registers the routes with the provider and prints one JSON line per source.
|
|
16
|
+
|
|
17
|
+
`KyselyIncomingWebhookService` (with the `incoming-webhook` schema) records a receipt per event, drops a provider's redelivery of an event it already accepted, and keeps each dispatch's attempts and last error. `pikku dev` and `pikku serve` use it when a Kysely database is configured.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [5e93f30]
|
|
20
|
+
- Updated dependencies [5e93f30]
|
|
21
|
+
- @pikku/core@0.12.125
|
|
22
|
+
|
|
23
|
+
## 0.12.90
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 4e05a10: Print the TypeScript text for a Zod schema directly, dropping `zod-to-ts`.
|
|
28
|
+
|
|
29
|
+
`processZodSchema` built a TypeScript AST only to print it straight back to a
|
|
30
|
+
string. `zodToTypeText` walks Zod's own definitions instead, which removes
|
|
31
|
+
`zod-to-ts`, `ts.createPrinter`, `ts.EmitHint` and `ts.createSourceFile` from
|
|
32
|
+
the Zod path — it no longer touches the compiler API at all. `@pikku/cli`
|
|
33
|
+
declared `zod-to-ts` without importing it; that is dropped too.
|
|
34
|
+
|
|
35
|
+
A defaulted field is now optional in the generated type. `processZodSchema`
|
|
36
|
+
already strips defaulted fields out of the JSON Schema's `required`, so the two
|
|
37
|
+
halves of the same contract disagreed: the validator accepted a payload that
|
|
38
|
+
omitted the field, while the type said a caller had to pass it.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [1394385]
|
|
41
|
+
- @pikku/core@0.12.124
|
|
42
|
+
|
|
1
43
|
## 0.12.89
|
|
2
44
|
|
|
3
45
|
### Patch Changes
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A call is the pikku one when the symbol it resolves to is, whatever the file
|
|
5
|
+
* chose to call it locally — `import { defineAnalyticsEvents as declare }` is
|
|
6
|
+
* an ordinary thing to write, and matching on the callee's text alone both
|
|
7
|
+
* misses that and claims a same-named local helper.
|
|
8
|
+
*/
|
|
9
|
+
export declare const callsImportedDefiner: (expression: ts.Expression, checker: ts.TypeChecker, definer: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The name the generated ingest has to import the declaration by — which is
|
|
12
|
+
* the name the module exports it under, not the name of the const. A module
|
|
13
|
+
* that keeps the declaration to itself has no such name, so it is refused here
|
|
14
|
+
* rather than emitted into an ingest that cannot compile.
|
|
15
|
+
*/
|
|
16
|
+
export declare const exportedName: (declaration: ts.VariableDeclaration, name: ts.Identifier, checker: ts.TypeChecker) => string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* An event's props, as source text rather than a resolved type — `z.string()`
|
|
19
|
+
* is what the declaration says and what a reader recognises, and resolving it
|
|
20
|
+
* would mean type-checking a third-party generic to render one table cell.
|
|
21
|
+
*
|
|
22
|
+
* Absent for a schema pikku cannot read the shape off (a shared const, a union,
|
|
23
|
+
* a vendor that is not zod). The catalog renders nothing rather than guessing.
|
|
24
|
+
*/
|
|
25
|
+
export declare const readEventShape: (initializer: ts.Expression) => Record<string, string> | undefined;
|
|
3
26
|
/**
|
|
4
27
|
* Record where each `defineAnalyticsEvents` declaration is, which names it
|
|
5
28
|
* declares, and the shape each name validates — the names drive the generated
|
|
@@ -7,18 +7,17 @@ const DEFINE_ANALYTICS_EVENTS = 'defineAnalyticsEvents';
|
|
|
7
7
|
* an ordinary thing to write, and matching on the callee's text alone both
|
|
8
8
|
* misses that and claims a same-named local helper.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
10
|
+
export const callsImportedDefiner = (expression, checker, definer) => {
|
|
11
11
|
if (!ts.isIdentifier(expression))
|
|
12
12
|
return false;
|
|
13
13
|
const symbol = checker.getSymbolAtLocation(expression);
|
|
14
14
|
if (!symbol)
|
|
15
|
-
return expression.text ===
|
|
15
|
+
return expression.text === definer;
|
|
16
16
|
// An import specifier names what it imported even when the module itself
|
|
17
17
|
// does not resolve, which is every project whose deps are not installed yet.
|
|
18
18
|
const declaration = symbol.declarations?.[0];
|
|
19
19
|
if (declaration && ts.isImportSpecifier(declaration)) {
|
|
20
|
-
return (
|
|
21
|
-
DEFINE_ANALYTICS_EVENTS);
|
|
20
|
+
return (declaration.propertyName ?? declaration.name).text === definer;
|
|
22
21
|
}
|
|
23
22
|
// Only an import can be pikku's. A file's own helper of the same name
|
|
24
23
|
// resolves to a symbol whose `.name` matches, so resolving without this
|
|
@@ -26,7 +25,7 @@ const callsDefineAnalyticsEvents = (expression, checker) => {
|
|
|
26
25
|
if (!(symbol.flags & ts.SymbolFlags.Alias))
|
|
27
26
|
return false;
|
|
28
27
|
const resolved = checker.getAliasedSymbol(symbol) ?? symbol;
|
|
29
|
-
return resolved.name ===
|
|
28
|
+
return resolved.name === definer;
|
|
30
29
|
};
|
|
31
30
|
/**
|
|
32
31
|
* The name the generated ingest has to import the declaration by — which is
|
|
@@ -34,7 +33,7 @@ const callsDefineAnalyticsEvents = (expression, checker) => {
|
|
|
34
33
|
* that keeps the declaration to itself has no such name, so it is refused here
|
|
35
34
|
* rather than emitted into an ingest that cannot compile.
|
|
36
35
|
*/
|
|
37
|
-
const exportedName = (declaration, name, checker) => {
|
|
36
|
+
export const exportedName = (declaration, name, checker) => {
|
|
38
37
|
const moduleSymbol = checker.getSymbolAtLocation(declaration.getSourceFile());
|
|
39
38
|
const declarationSymbol = checker.getSymbolAtLocation(name);
|
|
40
39
|
if (!moduleSymbol || !declarationSymbol) {
|
|
@@ -78,7 +77,7 @@ const findObjectShape = (expression) => {
|
|
|
78
77
|
* Absent for a schema pikku cannot read the shape off (a shared const, a union,
|
|
79
78
|
* a vendor that is not zod). The catalog renders nothing rather than guessing.
|
|
80
79
|
*/
|
|
81
|
-
const readEventShape = (initializer) => {
|
|
80
|
+
export const readEventShape = (initializer) => {
|
|
82
81
|
const shape = findObjectShape(initializer);
|
|
83
82
|
if (!shape)
|
|
84
83
|
return undefined;
|
|
@@ -110,7 +109,7 @@ export const addAnalytics = (logger, node, checker, state) => {
|
|
|
110
109
|
const { initializer, name } = node;
|
|
111
110
|
if (!initializer || !ts.isCallExpression(initializer))
|
|
112
111
|
return;
|
|
113
|
-
if (!
|
|
112
|
+
if (!callsImportedDefiner(initializer.expression, checker, DEFINE_ANALYTICS_EVENTS))
|
|
114
113
|
return;
|
|
115
114
|
if (!ts.isIdentifier(name))
|
|
116
115
|
return;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { callsImportedDefiner, exportedName, readEventShape, } from './add-analytics.js';
|
|
3
|
+
const DEFINE_OUTGOING_WEBHOOK = 'defineOutgoingWebhook';
|
|
4
|
+
const stringProperty = (object, key) => {
|
|
5
|
+
for (const property of object.properties) {
|
|
6
|
+
if (!ts.isPropertyAssignment(property))
|
|
7
|
+
continue;
|
|
8
|
+
const name = property.name;
|
|
9
|
+
const text = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
10
|
+
if (text !== key)
|
|
11
|
+
continue;
|
|
12
|
+
const value = property.initializer;
|
|
13
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
14
|
+
return value.text;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
const propertyInitializer = (object, key) => {
|
|
20
|
+
for (const property of object.properties) {
|
|
21
|
+
if (!ts.isPropertyAssignment(property))
|
|
22
|
+
continue;
|
|
23
|
+
const name = property.name;
|
|
24
|
+
if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) &&
|
|
25
|
+
name.text === key)
|
|
26
|
+
return property.initializer;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
export const addOutgoingWebhook = (logger, node, checker, state) => {
|
|
31
|
+
if (!ts.isVariableDeclaration(node))
|
|
32
|
+
return;
|
|
33
|
+
const { initializer, name } = node;
|
|
34
|
+
if (!initializer || !ts.isCallExpression(initializer))
|
|
35
|
+
return;
|
|
36
|
+
if (!callsImportedDefiner(initializer.expression, checker, DEFINE_OUTGOING_WEBHOOK))
|
|
37
|
+
return;
|
|
38
|
+
if (!ts.isIdentifier(name))
|
|
39
|
+
return;
|
|
40
|
+
const file = node.getSourceFile().fileName;
|
|
41
|
+
const [argument] = initializer.arguments;
|
|
42
|
+
if (!argument || !ts.isObjectLiteralExpression(argument)) {
|
|
43
|
+
logger.error(`defineOutgoingWebhook in ${file} must be called with an object literal: { event, title, payload }.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const event = stringProperty(argument, 'event');
|
|
47
|
+
const title = stringProperty(argument, 'title');
|
|
48
|
+
if (!event || !title) {
|
|
49
|
+
logger.error(`defineOutgoingWebhook '${name.text}' in ${file} needs 'event' and 'title' as string literals, so they can be listed without running the app.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const variable = exportedName(node, name, checker);
|
|
53
|
+
if (!variable) {
|
|
54
|
+
logger.error(`defineOutgoingWebhook '${event}' in ${file} is assigned to '${name.text}', which the module does not export. The generated webhook types import it by name, so export it.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const description = stringProperty(argument, 'description');
|
|
58
|
+
const payloadExpression = propertyInitializer(argument, 'payload');
|
|
59
|
+
const payload = payloadExpression
|
|
60
|
+
? readEventShape(payloadExpression)
|
|
61
|
+
: undefined;
|
|
62
|
+
const webhooks = (state.outgoingWebhooks ??= []);
|
|
63
|
+
const existing = webhooks.findIndex((webhook) => webhook.file === file && webhook.variable === variable);
|
|
64
|
+
const declared = webhooks.find((webhook, index) => webhook.event === event && index !== existing);
|
|
65
|
+
if (declared) {
|
|
66
|
+
logger.error(`Webhook event '${event}' is declared twice: ${declared.file} and ${file}. An event belongs to one defineOutgoingWebhook.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const entry = {
|
|
70
|
+
file,
|
|
71
|
+
variable,
|
|
72
|
+
event,
|
|
73
|
+
title,
|
|
74
|
+
...(description !== undefined ? { description } : {}),
|
|
75
|
+
...(payload ? { payload } : {}),
|
|
76
|
+
};
|
|
77
|
+
if (existing >= 0)
|
|
78
|
+
webhooks[existing] = entry;
|
|
79
|
+
else
|
|
80
|
+
webhooks.push(entry);
|
|
81
|
+
};
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { AddWiring } from '../types.js';
|
|
2
2
|
export declare const addTrigger: AddWiring;
|
|
3
|
+
/**
|
|
4
|
+
* A trigger named `<source>:<event>` must name an event its webhook source
|
|
5
|
+
* declares, or it would never fire.
|
|
6
|
+
*/
|
|
7
|
+
export declare const validateWebhookSourceTriggers: (logger: Parameters<AddWiring>[0], state: Parameters<AddWiring>[3]) => void;
|
package/dist/add/add-trigger.js
CHANGED
|
@@ -6,7 +6,9 @@ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js
|
|
|
6
6
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
7
7
|
import { extractWireNames } from '../utils/post-process.js';
|
|
8
8
|
import { resolveAddonName } from '../utils/resolve-addon-package.js';
|
|
9
|
+
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
|
|
9
10
|
import { ErrorCode } from '../error-codes.js';
|
|
11
|
+
const WEBHOOK_SOURCE_STEPS = ['receive', 'check', 'setup', 'teardown'];
|
|
10
12
|
export const addTrigger = (logger, node, checker, state, options) => {
|
|
11
13
|
if (!ts.isCallExpression(node)) {
|
|
12
14
|
return;
|
|
@@ -23,6 +25,9 @@ export const addTrigger = (logger, node, checker, state, options) => {
|
|
|
23
25
|
else if (expression.text === 'wireTriggerSource') {
|
|
24
26
|
addWireTriggerSource(logger, node, checker, state, options, firstArg);
|
|
25
27
|
}
|
|
28
|
+
else if (expression.text === 'wireTriggerWebhookSource') {
|
|
29
|
+
addWireTriggerWebhookSource(logger, node, checker, state, firstArg);
|
|
30
|
+
}
|
|
26
31
|
};
|
|
27
32
|
const addWireTrigger = (logger, node, checker, state, firstArg) => {
|
|
28
33
|
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
|
|
@@ -88,3 +93,102 @@ const addWireTriggerSource = (logger, node, checker, state, options, firstArg) =
|
|
|
88
93
|
}
|
|
89
94
|
state.triggers.files.add(node.getSourceFile().fileName);
|
|
90
95
|
};
|
|
96
|
+
const refTarget = (initializer) => {
|
|
97
|
+
if (!ts.isCallExpression(initializer) ||
|
|
98
|
+
!ts.isIdentifier(initializer.expression) ||
|
|
99
|
+
initializer.expression.text !== 'ref') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const [target] = initializer.arguments;
|
|
103
|
+
return target && ts.isStringLiteral(target) && target.text.includes(':')
|
|
104
|
+
? target.text
|
|
105
|
+
: null;
|
|
106
|
+
};
|
|
107
|
+
const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) => {
|
|
108
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const obj = firstArg;
|
|
112
|
+
const name = getPropertyValue(obj, 'name');
|
|
113
|
+
if (!name) {
|
|
114
|
+
logger.critical(ErrorCode.MISSING_NAME, `wireTriggerWebhookSource in ${node.getSourceFile().fileName} needs 'name' as a string literal.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const method = getPropertyValue(obj, 'method') ?? 'post';
|
|
118
|
+
const route = getPropertyValue(obj, 'route') ?? `/webhooks/${name}`;
|
|
119
|
+
let secret = getPropertyValue(obj, 'secret');
|
|
120
|
+
const eventsInitializer = getPropertyAssignmentInitializer(obj, 'events', false, checker);
|
|
121
|
+
const events = eventsInitializer
|
|
122
|
+
? checker
|
|
123
|
+
.getTypeAtLocation(eventsInitializer)
|
|
124
|
+
.getProperties()
|
|
125
|
+
.map((property) => property.getName())
|
|
126
|
+
.sort()
|
|
127
|
+
: [];
|
|
128
|
+
const steps = {};
|
|
129
|
+
for (const step of WEBHOOK_SOURCE_STEPS) {
|
|
130
|
+
const initializer = getPropertyAssignmentInitializer(obj, step, true, checker);
|
|
131
|
+
if (!initializer)
|
|
132
|
+
continue;
|
|
133
|
+
const target = refTarget(initializer);
|
|
134
|
+
if (target) {
|
|
135
|
+
if (!resolveFunctionMeta(state, target)) {
|
|
136
|
+
logger.warn(`Webhook source '${name}': addon function metadata for '${target}' is not available yet.`);
|
|
137
|
+
}
|
|
138
|
+
steps[step] = target;
|
|
139
|
+
state.serviceAggregation.usedFunctions.add(target);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const extracted = extractFunctionName(initializer, checker, state.rootDir);
|
|
143
|
+
let pikkuFuncId = extracted.pikkuFuncId;
|
|
144
|
+
if (pikkuFuncId.startsWith('__temp_')) {
|
|
145
|
+
pikkuFuncId = makeContextBasedId('trigger-webhook', name, step);
|
|
146
|
+
}
|
|
147
|
+
ensureInlineWiringFunction(state, pikkuFuncId, `${name}:${step}`, initializer, checker, extracted.isHelper);
|
|
148
|
+
const meta = state.functions.meta[pikkuFuncId];
|
|
149
|
+
if (meta && meta.sessionless === undefined)
|
|
150
|
+
meta.sessionless = true;
|
|
151
|
+
steps[step] = pikkuFuncId;
|
|
152
|
+
state.serviceAggregation.usedFunctions.add(pikkuFuncId);
|
|
153
|
+
}
|
|
154
|
+
// An addon's receive reads the secret by its own name; the app may have
|
|
155
|
+
// renamed it for this instance.
|
|
156
|
+
const namespace = steps.receive?.includes(':')
|
|
157
|
+
? steps.receive.slice(0, steps.receive.indexOf(':'))
|
|
158
|
+
: null;
|
|
159
|
+
const addon = namespace
|
|
160
|
+
? state.rpc.wireAddonDeclarations.get(namespace)
|
|
161
|
+
: undefined;
|
|
162
|
+
if (secret && addon?.secretOverrides?.[secret]) {
|
|
163
|
+
secret = addon.secretOverrides[secret];
|
|
164
|
+
}
|
|
165
|
+
const meta = {
|
|
166
|
+
name,
|
|
167
|
+
method: method,
|
|
168
|
+
route,
|
|
169
|
+
events,
|
|
170
|
+
...(secret ? { secret } : {}),
|
|
171
|
+
...steps,
|
|
172
|
+
};
|
|
173
|
+
state.triggers.webhookSourceMeta[name] = meta;
|
|
174
|
+
state.triggers.files.add(node.getSourceFile().fileName);
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* A trigger named `<source>:<event>` must name an event its webhook source
|
|
178
|
+
* declares, or it would never fire.
|
|
179
|
+
*/
|
|
180
|
+
export const validateWebhookSourceTriggers = (logger, state) => {
|
|
181
|
+
const sources = state.triggers.webhookSourceMeta;
|
|
182
|
+
for (const trigger of Object.keys(state.triggers.meta)) {
|
|
183
|
+
const colon = trigger.indexOf(':');
|
|
184
|
+
if (colon === -1)
|
|
185
|
+
continue;
|
|
186
|
+
const source = sources[trigger.slice(0, colon)];
|
|
187
|
+
if (!source || source.events.length === 0)
|
|
188
|
+
continue;
|
|
189
|
+
const event = trigger.slice(colon + 1);
|
|
190
|
+
if (!source.events.includes(event)) {
|
|
191
|
+
logger.error(`Trigger '${trigger}' listens for '${event}', which webhook source '${source.name}' does not declare. Declared: ${source.events.join(', ')}.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -26,3 +26,4 @@ export type { SerializedWorkflowGraph, SerializedWorkflowGraphs, } from './utils
|
|
|
26
26
|
export { resolveCoreType } from './utils/resolve-core-type.js';
|
|
27
27
|
export { readModuleSpecifiers } from './utils/read-module-specifiers.js';
|
|
28
28
|
export { readTsconfigOutDir } from './utils/read-tsconfig-out-dir.js';
|
|
29
|
+
export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
|
package/dist/index.js
CHANGED
|
@@ -15,3 +15,4 @@ export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
|
|
|
15
15
|
export { resolveCoreType } from './utils/resolve-core-type.js';
|
|
16
16
|
export { readModuleSpecifiers } from './utils/read-module-specifiers.js';
|
|
17
17
|
export { readTsconfigOutDir } from './utils/read-tsconfig-out-dir.js';
|
|
18
|
+
export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
|
package/dist/inspector.js
CHANGED
|
@@ -14,6 +14,7 @@ import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
|
|
|
14
14
|
import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
|
|
15
15
|
import { pikkuState } from '@pikku/core/state';
|
|
16
16
|
import { resolveLatestVersions } from './utils/resolve-versions.js';
|
|
17
|
+
import { validateWebhookSourceTriggers } from './add/add-trigger.js';
|
|
17
18
|
import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
|
|
18
19
|
import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
|
|
19
20
|
import { generateAllSchemas } from './utils/schema-generator.js';
|
|
@@ -79,6 +80,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
79
80
|
triggers: {
|
|
80
81
|
meta: {},
|
|
81
82
|
sourceMeta: {},
|
|
83
|
+
webhookSourceMeta: {},
|
|
82
84
|
files: new Set(),
|
|
83
85
|
},
|
|
84
86
|
scheduledTasks: {
|
|
@@ -353,6 +355,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
353
355
|
ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
|
|
354
356
|
}
|
|
355
357
|
logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
|
|
358
|
+
validateWebhookSourceTriggers(logger, state);
|
|
356
359
|
resolveLatestVersions(state, logger);
|
|
357
360
|
if (options.schemaConfig) {
|
|
358
361
|
const startSchemas = performance.now();
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
|
|
|
5
5
|
import type { GatewaysMeta } from '@pikku/core/gateway';
|
|
6
6
|
import type { HTTPWiringsMeta } from '@pikku/core/http';
|
|
7
7
|
import type { ScheduledTasksMeta } from '@pikku/core/scheduler';
|
|
8
|
-
import type { TriggerMeta, TriggerSourceMeta } from '@pikku/core/trigger';
|
|
8
|
+
import type { TriggerMeta, TriggerSourceMeta, WebhookSourcesMeta } from '@pikku/core/trigger';
|
|
9
9
|
import type { QueueWorkersMeta } from '@pikku/core/queue';
|
|
10
10
|
import type { WorkflowsMeta } from '@pikku/core/workflow';
|
|
11
11
|
import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
|
|
@@ -515,6 +515,14 @@ export interface InspectorState {
|
|
|
515
515
|
* inspector cannot read off the declaration. */
|
|
516
516
|
props?: Record<string, Record<string, string>>;
|
|
517
517
|
}>;
|
|
518
|
+
outgoingWebhooks?: Array<{
|
|
519
|
+
file: string;
|
|
520
|
+
variable: string;
|
|
521
|
+
event: string;
|
|
522
|
+
title: string;
|
|
523
|
+
description?: string;
|
|
524
|
+
payload?: Record<string, string>;
|
|
525
|
+
}>;
|
|
518
526
|
addonServerlessIncompatible: Map<string, string[]>;
|
|
519
527
|
configFactories: PathToNameAndType;
|
|
520
528
|
serverLifecycleFactories: PathToNameAndType;
|
|
@@ -539,6 +547,7 @@ export interface InspectorState {
|
|
|
539
547
|
triggers: {
|
|
540
548
|
meta: TriggerMeta;
|
|
541
549
|
sourceMeta: TriggerSourceMeta;
|
|
550
|
+
webhookSourceMeta: WebhookSourcesMeta;
|
|
542
551
|
files: Set<string>;
|
|
543
552
|
};
|
|
544
553
|
scheduledTasks: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AddonResolver = {
|
|
2
|
+
resolve: (id: string) => string;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* An addon is installed wherever the package that calls `wireAddon` lists it,
|
|
6
|
+
* which in a workspace is usually not the repo root. Resolve from the declaring
|
|
7
|
+
* package first, as its code does at runtime, then from the root.
|
|
8
|
+
*/
|
|
9
|
+
export declare const addonResolutionDirs: (rootDir: string, declFile?: string) => string[];
|
|
10
|
+
export declare const createAddonResolver: (dirs: string[]) => AddonResolver;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { join, dirname, parse, resolve } from 'path';
|
|
4
|
+
const nearestPackageDir = (file) => {
|
|
5
|
+
const root = parse(file).root;
|
|
6
|
+
let dir = dirname(file);
|
|
7
|
+
while (dir && dir !== root) {
|
|
8
|
+
if (existsSync(join(dir, 'package.json')))
|
|
9
|
+
return dir;
|
|
10
|
+
const parent = dirname(dir);
|
|
11
|
+
if (parent === dir)
|
|
12
|
+
break;
|
|
13
|
+
dir = parent;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* An addon is installed wherever the package that calls `wireAddon` lists it,
|
|
19
|
+
* which in a workspace is usually not the repo root. Resolve from the declaring
|
|
20
|
+
* package first, as its code does at runtime, then from the root.
|
|
21
|
+
*/
|
|
22
|
+
export const addonResolutionDirs = (rootDir, declFile) => {
|
|
23
|
+
const root = resolve(rootDir);
|
|
24
|
+
const declDir = declFile ? nearestPackageDir(resolve(declFile)) : null;
|
|
25
|
+
return declDir && declDir !== root ? [declDir, root] : [root];
|
|
26
|
+
};
|
|
27
|
+
export const createAddonResolver = (dirs) => {
|
|
28
|
+
const requires = dirs.map((dir) => createRequire(join(dir, 'package.json')));
|
|
29
|
+
return {
|
|
30
|
+
resolve: (id) => {
|
|
31
|
+
let lastError;
|
|
32
|
+
for (const req of requires) {
|
|
33
|
+
try {
|
|
34
|
+
return req.resolve(id);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
lastError = e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw lastError;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* contract has to describe one shape. Zod says so twice, in two different
|
|
7
7
|
* places, and neither says it usefully on its own — `toJSONSchema` with
|
|
8
8
|
* `unrepresentable: 'any'` quietly yields `{}`, a schema that accepts
|
|
9
|
-
* everything, and `
|
|
10
|
-
*
|
|
11
|
-
* and the call.
|
|
9
|
+
* everything, and `zodToTypeText` prints the input side of the pipe, which is
|
|
10
|
+
* not the shape the function receives. Finding it up front lets the diagnostic
|
|
11
|
+
* name the schema, the path, and the call.
|
|
12
12
|
*
|
|
13
13
|
* Zod 4 models `x.transform(fn)` as a `pipe` whose `out` is a `transform`, so
|
|
14
14
|
* the walk looks for that node rather than for the method.
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* contract has to describe one shape. Zod says so twice, in two different
|
|
7
7
|
* places, and neither says it usefully on its own — `toJSONSchema` with
|
|
8
8
|
* `unrepresentable: 'any'` quietly yields `{}`, a schema that accepts
|
|
9
|
-
* everything, and `
|
|
10
|
-
*
|
|
11
|
-
* and the call.
|
|
9
|
+
* everything, and `zodToTypeText` prints the input side of the pipe, which is
|
|
10
|
+
* not the shape the function receives. Finding it up front lets the diagnostic
|
|
11
|
+
* name the schema, the path, and the call.
|
|
12
12
|
*
|
|
13
13
|
* Zod 4 models `x.transform(fn)` as a `pipe` whose `out` is a `transform`, so
|
|
14
14
|
* the walk looks for that node rather than for the method.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
3
|
import { join, dirname, parse, relative } from 'path';
|
|
4
|
+
import { addonResolutionDirs, createAddonResolver, } from './addon-resolution.js';
|
|
5
5
|
import { ErrorCode } from '../error-codes.js';
|
|
6
6
|
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
7
7
|
value !== null &&
|
|
@@ -90,45 +90,6 @@ const resolveAddonMeta = (require, packageName, baseName) => {
|
|
|
90
90
|
}
|
|
91
91
|
return null;
|
|
92
92
|
};
|
|
93
|
-
const nearestPackageDir = (file) => {
|
|
94
|
-
const root = parse(file).root;
|
|
95
|
-
let dir = dirname(file);
|
|
96
|
-
while (dir && dir !== root) {
|
|
97
|
-
if (existsSync(join(dir, 'package.json')))
|
|
98
|
-
return dir;
|
|
99
|
-
const parent = dirname(dir);
|
|
100
|
-
if (parent === dir)
|
|
101
|
-
break;
|
|
102
|
-
dir = parent;
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* An addon is installed wherever the package that calls `wireAddon` lists it,
|
|
108
|
-
* which in a workspace is usually not the repo root. Resolve from the declaring
|
|
109
|
-
* package first, as its code does at runtime, then from the root.
|
|
110
|
-
*/
|
|
111
|
-
const addonResolutionDirs = (rootDir, declFile) => {
|
|
112
|
-
const declDir = declFile ? nearestPackageDir(declFile) : null;
|
|
113
|
-
return declDir && declDir !== rootDir ? [declDir, rootDir] : [rootDir];
|
|
114
|
-
};
|
|
115
|
-
const createAddonResolver = (dirs) => {
|
|
116
|
-
const requires = dirs.map((dir) => createRequire(join(dir, 'package.json')));
|
|
117
|
-
return {
|
|
118
|
-
resolve: (id) => {
|
|
119
|
-
let lastError;
|
|
120
|
-
for (const req of requires) {
|
|
121
|
-
try {
|
|
122
|
-
return req.resolve(id);
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
lastError = e;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
throw lastError;
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
93
|
const findInstalledPackageDir = (dirs, packageName) => {
|
|
133
94
|
for (const start of dirs) {
|
|
134
95
|
const root = parse(start).root;
|
|
@@ -10,6 +10,7 @@ import { relative } from 'node:path';
|
|
|
10
10
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
|
|
11
11
|
import { flattenScopeDefinitions } from '@pikku/core/scope';
|
|
12
12
|
import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js';
|
|
13
|
+
import { addonResolutionDirs } from './addon-resolution.js';
|
|
13
14
|
/**
|
|
14
15
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
15
16
|
* handler's function meta.
|
|
@@ -208,6 +209,15 @@ export function aggregateRequiredServices(state) {
|
|
|
208
209
|
requiredServices.add('schedulerService');
|
|
209
210
|
requiredServices.add('queueService');
|
|
210
211
|
}
|
|
212
|
+
// Webhook source routes and their worker are generated and destructure
|
|
213
|
+
// nothing, so they are found by id: the route accepts into the queue through
|
|
214
|
+
// incomingWebhookService and the worker records attempts through it.
|
|
215
|
+
const hasWebhookSources = Object.values(state.triggers?.webhookSourceMeta ?? {}).some(({ method, route }) => state.functions.meta[`http:${method}:${route}`] !== undefined);
|
|
216
|
+
if (hasWebhookSources ||
|
|
217
|
+
state.functions.meta['queue:pikku-incoming-webhooks'] !== undefined) {
|
|
218
|
+
requiredServices.add('incomingWebhookService');
|
|
219
|
+
requiredServices.add('queueService');
|
|
220
|
+
}
|
|
211
221
|
// 6b. Inject synthetic queue workers for workflow graph steps.
|
|
212
222
|
// Each workflow gets an orchestrator queue and per-step queues.
|
|
213
223
|
// Without these, the PikkuWorkflowService constructor can't find
|
|
@@ -451,32 +461,34 @@ export function validateVariableOverrides(logger, state) {
|
|
|
451
461
|
}
|
|
452
462
|
}
|
|
453
463
|
/**
|
|
454
|
-
* Can `pkg` be resolved from `
|
|
464
|
+
* Can `pkg` be resolved from any of `dirs`, by any means the loader would use?
|
|
455
465
|
*
|
|
456
466
|
* Resolution is attempted against `<pkg>/package.json` first and the bare
|
|
457
467
|
* specifier second, because neither alone is conclusive. A package with a
|
|
458
468
|
* restrictive `exports` map refuses the first with
|
|
459
469
|
* `ERR_PACKAGE_PATH_NOT_EXPORTED` — which is not a failure at all: reaching the
|
|
460
|
-
* exports map means the package was found. Only a module-not-found from
|
|
461
|
-
*
|
|
470
|
+
* exports map means the package was found. Only a module-not-found from every
|
|
471
|
+
* attempt means it is genuinely absent.
|
|
462
472
|
*/
|
|
463
|
-
const isPackageResolvable = (pkg,
|
|
473
|
+
const isPackageResolvable = (pkg, dirs) => {
|
|
464
474
|
const NOT_FOUND = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND']);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
req = createRequire(join(rootDir, 'package.json'));
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
return true; // cannot ask; do not accuse
|
|
471
|
-
}
|
|
472
|
-
for (const specifier of [`${pkg}/package.json`, pkg]) {
|
|
475
|
+
for (const dir of dirs) {
|
|
476
|
+
let req;
|
|
473
477
|
try {
|
|
474
|
-
req.
|
|
475
|
-
return true;
|
|
478
|
+
req = createRequire(join(dir, 'package.json'));
|
|
476
479
|
}
|
|
477
|
-
catch
|
|
478
|
-
|
|
480
|
+
catch {
|
|
481
|
+
return true; // cannot ask; do not accuse
|
|
482
|
+
}
|
|
483
|
+
for (const specifier of [`${pkg}/package.json`, pkg]) {
|
|
484
|
+
try {
|
|
485
|
+
req.resolve(specifier);
|
|
479
486
|
return true;
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
if (!NOT_FOUND.has(e?.code))
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
494
|
return false;
|
|
@@ -514,35 +526,45 @@ export function validateRemoteAddonDependencies(logger, state) {
|
|
|
514
526
|
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
515
527
|
if (decl.remote)
|
|
516
528
|
continue;
|
|
517
|
-
|
|
529
|
+
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
530
|
+
if (isPackageResolvable(decl.package, dirs))
|
|
518
531
|
continue;
|
|
519
|
-
logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime.
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
532
|
+
logger.critical(ErrorCode.ADDON_NOT_INSTALLED, `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved from ${dirs.join(' or ')} — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Add it to the dependencies of ${join(dirs[0], 'package.json')} and install, or remove the wireAddon call.`);
|
|
533
|
+
}
|
|
534
|
+
// A remote addon belongs in the devDependencies of the package that wires
|
|
535
|
+
// it, which in a workspace is not the root.
|
|
536
|
+
const manifests = new Map();
|
|
537
|
+
const readManifest = (path) => {
|
|
538
|
+
if (manifests.has(path))
|
|
539
|
+
return manifests.get(path);
|
|
540
|
+
let manifest = null;
|
|
541
|
+
if (existsSync(path)) {
|
|
542
|
+
try {
|
|
543
|
+
manifest = JSON.parse(readFileSync(path, 'utf-8'));
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
logger.warn(`Could not read ${path} to verify remote addon dependencies: ${e?.message ?? e}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
manifests.set(path, manifest);
|
|
550
|
+
return manifest;
|
|
551
|
+
};
|
|
536
552
|
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
537
553
|
if (!decl.remote)
|
|
538
554
|
continue;
|
|
555
|
+
const pkgJsonPath = join(addonResolutionDirs(state.rootDir, decl.file)[0], 'package.json');
|
|
556
|
+
const pkgJson = readManifest(pkgJsonPath);
|
|
557
|
+
if (!pkgJson)
|
|
558
|
+
continue; // no manifest to check (e.g. some tests)
|
|
559
|
+
const prodDeps = pkgJson.dependencies ?? {};
|
|
560
|
+
const devDeps = pkgJson.devDependencies ?? {};
|
|
539
561
|
if (decl.package in devDeps)
|
|
540
562
|
continue; // correct
|
|
541
563
|
if (decl.package in prodDeps) {
|
|
542
|
-
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is a production dependency, but wireRemoteAddon consumes it for types only — its handlers run on the host. Move '${decl.package}' from "dependencies" to "devDependencies".`);
|
|
564
|
+
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is a production dependency in ${pkgJsonPath}, but wireRemoteAddon consumes it for types only — its handlers run on the host. Move '${decl.package}' from "dependencies" to "devDependencies".`);
|
|
543
565
|
}
|
|
544
566
|
else {
|
|
545
|
-
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is wired with wireRemoteAddon but is not in "devDependencies". Add '${decl.package}' to "devDependencies" (types only).`);
|
|
567
|
+
logger.critical(ErrorCode.REMOTE_ADDON_NOT_DEV_DEPENDENCY, `Remote addon '${namespace}' ('${decl.package}') is wired with wireRemoteAddon but is not in the "devDependencies" of ${pkgJsonPath}. Add '${decl.package}' to "devDependencies" (types only).`);
|
|
546
568
|
}
|
|
547
569
|
}
|
|
548
570
|
}
|
|
@@ -169,6 +169,12 @@ function updateWiringReferences(state, oldId, newId) {
|
|
|
169
169
|
source.pikkuFuncId = newId;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
for (const source of Object.values(state.triggers.webhookSourceMeta ?? {})) {
|
|
173
|
+
for (const step of ['receive', 'check', 'setup', 'teardown']) {
|
|
174
|
+
if (source[step] === oldId)
|
|
175
|
+
source[step] = newId;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
172
178
|
}
|
|
173
179
|
// MCP resources, tools, and prompts
|
|
174
180
|
if (state.mcpEndpoints) {
|
|
@@ -5,8 +5,8 @@ import { dirname, join, resolve } from 'path';
|
|
|
5
5
|
import { createGenerator, RootlessError } from 'ts-json-schema-generator';
|
|
6
6
|
import { register } from 'tsx/esm/api';
|
|
7
7
|
import * as z from 'zod';
|
|
8
|
-
import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts';
|
|
9
8
|
import { findZodTransform } from './find-zod-transform.js';
|
|
9
|
+
import { zodToTypeText } from './zod-to-type-text.js';
|
|
10
10
|
import { ErrorCode } from '../error-codes.js';
|
|
11
11
|
import { generateCustomTypes } from './custom-types-generator.js';
|
|
12
12
|
const PRIMITIVE_TYPES = new Set([
|
|
@@ -382,7 +382,7 @@ async function importWithRegister(sourceFile) {
|
|
|
382
382
|
void Promise.resolve(unregister()).catch(() => { });
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
|
-
function processZodSchema(schemaName, zodSchema, schemas, typesMap,
|
|
385
|
+
function processZodSchema(schemaName, zodSchema, schemas, typesMap, logger) {
|
|
386
386
|
const transformPath = findZodTransform(zodSchema);
|
|
387
387
|
if (transformPath !== undefined) {
|
|
388
388
|
const where = transformPath === '' ? 'the schema itself' : transformPath;
|
|
@@ -412,18 +412,13 @@ function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTyp
|
|
|
412
412
|
delete schema.required;
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
-
|
|
416
|
-
const typeText = printer.printNode(ts.EmitHint.Unspecified, tsType, fakeSourceFile);
|
|
417
|
-
typesMap.addCustomType(schemaName, typeText, []);
|
|
415
|
+
typesMap.addCustomType(schemaName, zodToTypeText(zodSchema), []);
|
|
418
416
|
schemas[schemaName] = schema;
|
|
419
417
|
logger.debug(`• Generated schema from Zod: ${schemaName}`);
|
|
420
418
|
}
|
|
421
419
|
async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
422
420
|
const schemas = {};
|
|
423
421
|
const errors = [];
|
|
424
|
-
const auxiliaryTypeStore = createAuxiliaryTypeStore();
|
|
425
|
-
const printer = ts.createPrinter();
|
|
426
|
-
const fakeSourceFile = ts.createSourceFile('zod-types.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
|
|
427
422
|
// Validate all schemas are zod (or unspecified vendor)
|
|
428
423
|
for (const [schemaName, ref] of schemaLookup.entries()) {
|
|
429
424
|
if (ref.vendor && ref.vendor !== 'zod') {
|
|
@@ -453,7 +448,7 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
453
448
|
continue;
|
|
454
449
|
}
|
|
455
450
|
try {
|
|
456
|
-
processZodSchema(schemaName, zodSchema, schemas, typesMap,
|
|
451
|
+
processZodSchema(schemaName, zodSchema, schemas, typesMap, logger);
|
|
457
452
|
}
|
|
458
453
|
catch (e) {
|
|
459
454
|
errors.push(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
|
|
@@ -478,7 +473,7 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
478
473
|
errors.push(`Could not find exported schema '${ref.variableName}' in ${runtimeFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`);
|
|
479
474
|
continue;
|
|
480
475
|
}
|
|
481
|
-
processZodSchema(schemaName, zodSchema, schemas, typesMap,
|
|
476
|
+
processZodSchema(schemaName, zodSchema, schemas, typesMap, logger);
|
|
482
477
|
}
|
|
483
478
|
catch (e) {
|
|
484
479
|
errors.push(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
|
|
@@ -143,6 +143,7 @@ export interface SerializableInspectorState {
|
|
|
143
143
|
triggers: {
|
|
144
144
|
meta: InspectorState['triggers']['meta'];
|
|
145
145
|
sourceMeta: InspectorState['triggers']['sourceMeta'];
|
|
146
|
+
webhookSourceMeta?: InspectorState['triggers']['webhookSourceMeta'];
|
|
146
147
|
files: string[];
|
|
147
148
|
};
|
|
148
149
|
scheduledTasks: {
|
|
@@ -56,6 +56,7 @@ export function serializeInspectorState(state) {
|
|
|
56
56
|
triggers: {
|
|
57
57
|
meta: state.triggers.meta,
|
|
58
58
|
sourceMeta: state.triggers.sourceMeta,
|
|
59
|
+
webhookSourceMeta: state.triggers.webhookSourceMeta,
|
|
59
60
|
files: Array.from(state.triggers.files),
|
|
60
61
|
},
|
|
61
62
|
scheduledTasks: {
|
|
@@ -243,6 +244,7 @@ export function deserializeInspectorState(data) {
|
|
|
243
244
|
triggers: {
|
|
244
245
|
meta: data.triggers?.meta ?? {},
|
|
245
246
|
sourceMeta: data.triggers?.sourceMeta ?? {},
|
|
247
|
+
webhookSourceMeta: data.triggers?.webhookSourceMeta ?? {},
|
|
246
248
|
files: new Set(data.triggers?.files ?? []),
|
|
247
249
|
},
|
|
248
250
|
scheduledTasks: {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print the TypeScript type text for a Zod schema.
|
|
3
|
+
*
|
|
4
|
+
* `processZodSchema` needs two things from a schema: the JSON Schema, which
|
|
5
|
+
* `z.toJSONSchema` already gives it, and the TypeScript source text that goes
|
|
6
|
+
* into the generated types. Only the second needed a library, and only to build
|
|
7
|
+
* a TypeScript AST that was immediately printed back to a string — so this walks
|
|
8
|
+
* Zod's own definitions and writes the text directly.
|
|
9
|
+
*
|
|
10
|
+
* It prints from the Zod tree rather than from the JSON Schema on purpose. The
|
|
11
|
+
* JSON Schema has already lost what the text needs: `z.date()` is rewritten to
|
|
12
|
+
* `string`/`date-time` before anything can read it, and `z.any()` and
|
|
13
|
+
* `z.unknown()` both flatten to `{}`. The Zod tree still says `Date`, `any` and
|
|
14
|
+
* `unknown`.
|
|
15
|
+
*
|
|
16
|
+
* The walk mirrors {@link findZodTransform} — same `_zod.def` shapes, same
|
|
17
|
+
* seen-set and node budget, for the same reason.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The TypeScript type text for `schema`, as it would be written by hand.
|
|
21
|
+
*
|
|
22
|
+
* A schema that refers to itself cannot be written inline, so a cycle prints as
|
|
23
|
+
* `any` rather than recursing forever.
|
|
24
|
+
*/
|
|
25
|
+
export declare function zodToTypeText(schema: unknown): string;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print the TypeScript type text for a Zod schema.
|
|
3
|
+
*
|
|
4
|
+
* `processZodSchema` needs two things from a schema: the JSON Schema, which
|
|
5
|
+
* `z.toJSONSchema` already gives it, and the TypeScript source text that goes
|
|
6
|
+
* into the generated types. Only the second needed a library, and only to build
|
|
7
|
+
* a TypeScript AST that was immediately printed back to a string — so this walks
|
|
8
|
+
* Zod's own definitions and writes the text directly.
|
|
9
|
+
*
|
|
10
|
+
* It prints from the Zod tree rather than from the JSON Schema on purpose. The
|
|
11
|
+
* JSON Schema has already lost what the text needs: `z.date()` is rewritten to
|
|
12
|
+
* `string`/`date-time` before anything can read it, and `z.any()` and
|
|
13
|
+
* `z.unknown()` both flatten to `{}`. The Zod tree still says `Date`, `any` and
|
|
14
|
+
* `unknown`.
|
|
15
|
+
*
|
|
16
|
+
* The walk mirrors {@link findZodTransform} — same `_zod.def` shapes, same
|
|
17
|
+
* seen-set and node budget, for the same reason.
|
|
18
|
+
*/
|
|
19
|
+
/** Max nodes visited, so a pathological or cyclic schema cannot hang the walk. */
|
|
20
|
+
const MAX_NODES = 10_000;
|
|
21
|
+
const INDENT = ' ';
|
|
22
|
+
const isNode = (value) => typeof value === 'object' &&
|
|
23
|
+
value !== null &&
|
|
24
|
+
typeof value._zod?.def?.type === 'string';
|
|
25
|
+
const tight = (text) => ({ text, loose: false });
|
|
26
|
+
const loose = (text) => ({ text, loose: true });
|
|
27
|
+
const parenthesized = ({ text, loose }) => loose ? `(${text})` : text;
|
|
28
|
+
/** A JS value as a TypeScript literal type. */
|
|
29
|
+
const literalOf = (value) => {
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
if (typeof value === 'bigint')
|
|
33
|
+
return `${value}n`;
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return 'undefined';
|
|
36
|
+
if (value === null)
|
|
37
|
+
return 'null';
|
|
38
|
+
return String(value);
|
|
39
|
+
};
|
|
40
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
41
|
+
const propertyKey = (name) => IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
42
|
+
/** A `.describe()` string as a JSDoc block at the given indentation. */
|
|
43
|
+
const jsDoc = (description, indent) => {
|
|
44
|
+
if (!description)
|
|
45
|
+
return '';
|
|
46
|
+
const lines = description.split('\n');
|
|
47
|
+
if (lines.length === 1)
|
|
48
|
+
return `${indent}/** ${lines[0]} */\n`;
|
|
49
|
+
return `${indent}/**\n${lines.map((line) => `${indent} * ${line}`).join('\n')}\n${indent} */\n`;
|
|
50
|
+
};
|
|
51
|
+
const union = (parts) => {
|
|
52
|
+
const texts = [...new Set(parts.map(parenthesized))];
|
|
53
|
+
if (texts.length === 0)
|
|
54
|
+
return tight('never');
|
|
55
|
+
if (texts.length === 1)
|
|
56
|
+
return tight(texts[0]);
|
|
57
|
+
return loose(texts.join(' | '));
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The TypeScript type text for `schema`, as it would be written by hand.
|
|
61
|
+
*
|
|
62
|
+
* A schema that refers to itself cannot be written inline, so a cycle prints as
|
|
63
|
+
* `any` rather than recursing forever.
|
|
64
|
+
*/
|
|
65
|
+
export function zodToTypeText(schema) {
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
let budget = MAX_NODES;
|
|
68
|
+
const print = (node, indent) => {
|
|
69
|
+
if (budget-- <= 0 || !isNode(node))
|
|
70
|
+
return tight('any');
|
|
71
|
+
if (seen.has(node))
|
|
72
|
+
return tight('any');
|
|
73
|
+
seen.add(node);
|
|
74
|
+
try {
|
|
75
|
+
return printDef(node, indent);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
seen.delete(node);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const printDef = (node, indent) => {
|
|
82
|
+
const def = node._zod.def;
|
|
83
|
+
const inner = indent + INDENT;
|
|
84
|
+
switch (def.type) {
|
|
85
|
+
case 'string':
|
|
86
|
+
case 'email':
|
|
87
|
+
case 'url':
|
|
88
|
+
case 'uuid':
|
|
89
|
+
case 'guid':
|
|
90
|
+
case 'cuid':
|
|
91
|
+
case 'cuid2':
|
|
92
|
+
case 'ulid':
|
|
93
|
+
case 'emoji':
|
|
94
|
+
case 'nanoid':
|
|
95
|
+
case 'jwt':
|
|
96
|
+
case 'base64':
|
|
97
|
+
case 'base64url':
|
|
98
|
+
case 'ipv4':
|
|
99
|
+
case 'ipv6':
|
|
100
|
+
case 'cidrv4':
|
|
101
|
+
case 'cidrv6':
|
|
102
|
+
case 'iso_date':
|
|
103
|
+
case 'iso_time':
|
|
104
|
+
case 'iso_datetime':
|
|
105
|
+
case 'iso_duration':
|
|
106
|
+
return tight('string');
|
|
107
|
+
case 'number':
|
|
108
|
+
case 'int':
|
|
109
|
+
case 'nan':
|
|
110
|
+
return tight('number');
|
|
111
|
+
case 'bigint':
|
|
112
|
+
return tight('bigint');
|
|
113
|
+
case 'boolean':
|
|
114
|
+
return tight('boolean');
|
|
115
|
+
case 'date':
|
|
116
|
+
return tight('Date');
|
|
117
|
+
case 'null':
|
|
118
|
+
return tight('null');
|
|
119
|
+
case 'undefined':
|
|
120
|
+
case 'void':
|
|
121
|
+
return tight('undefined');
|
|
122
|
+
case 'symbol':
|
|
123
|
+
return tight('symbol');
|
|
124
|
+
case 'file':
|
|
125
|
+
return tight('File');
|
|
126
|
+
case 'never':
|
|
127
|
+
return tight('never');
|
|
128
|
+
case 'any':
|
|
129
|
+
return tight('any');
|
|
130
|
+
case 'unknown':
|
|
131
|
+
return tight('unknown');
|
|
132
|
+
case 'literal':
|
|
133
|
+
return union((def.values ?? []).map((v) => tight(literalOf(v))));
|
|
134
|
+
case 'enum':
|
|
135
|
+
return union(Object.values(def.entries ?? {}).map((v) => tight(literalOf(v))));
|
|
136
|
+
case 'template_literal': {
|
|
137
|
+
const body = (def.parts ?? [])
|
|
138
|
+
.map((part) => isNode(part) ? `\${${print(part, indent).text}}` : String(part))
|
|
139
|
+
.join('');
|
|
140
|
+
return tight(`\`${body}\``);
|
|
141
|
+
}
|
|
142
|
+
case 'array':
|
|
143
|
+
return tight(`${parenthesized(print(def.element, indent))}[]`);
|
|
144
|
+
case 'set':
|
|
145
|
+
return tight(`Set<${print(def.valueType, indent).text}>`);
|
|
146
|
+
case 'map':
|
|
147
|
+
return tight(`Map<${print(def.keyType, indent).text}, ${print(def.valueType, indent).text}>`);
|
|
148
|
+
case 'promise':
|
|
149
|
+
return tight(`Promise<${print(def.innerType, indent).text}>`);
|
|
150
|
+
case 'tuple': {
|
|
151
|
+
const items = (def.items ?? []).map((item) => print(item, inner).text);
|
|
152
|
+
if (def.rest) {
|
|
153
|
+
items.push(`...${parenthesized(print(def.rest, inner))}[]`);
|
|
154
|
+
}
|
|
155
|
+
if (items.length === 0)
|
|
156
|
+
return tight('[]');
|
|
157
|
+
return tight(`[\n${items.map((item) => `${inner}${item}`).join(',\n')}\n${indent}]`);
|
|
158
|
+
}
|
|
159
|
+
case 'union':
|
|
160
|
+
return union((def.options ?? []).map((option) => print(option, indent)));
|
|
161
|
+
case 'intersection':
|
|
162
|
+
return loose(`${parenthesized(print(def.left, indent))} & ${parenthesized(print(def.right, indent))}`);
|
|
163
|
+
case 'record':
|
|
164
|
+
return tight(`{\n${inner}[key: ${print(def.keyType, inner).text}]: ${print(def.valueType, inner).text};\n${indent}}`);
|
|
165
|
+
case 'object':
|
|
166
|
+
case 'interface': {
|
|
167
|
+
const shape = def.shape ?? {};
|
|
168
|
+
const entries = Object.entries(shape);
|
|
169
|
+
if (entries.length === 0 && !def.catchall)
|
|
170
|
+
return tight('{}');
|
|
171
|
+
const members = entries.map(([name, property]) => {
|
|
172
|
+
const optional = isNode(property) && isOptional(property);
|
|
173
|
+
const printed = print(property, inner);
|
|
174
|
+
const type = optional
|
|
175
|
+
? `${parenthesized(printed)} | undefined`
|
|
176
|
+
: printed.text;
|
|
177
|
+
const doc = jsDoc(isNode(property) ? property.description : undefined, inner);
|
|
178
|
+
return `${doc}${inner}${propertyKey(name)}${optional ? '?' : ''}: ${type};`;
|
|
179
|
+
});
|
|
180
|
+
if (def.catchall) {
|
|
181
|
+
members.push(`${inner}[key: string]: ${print(def.catchall, inner).text};`);
|
|
182
|
+
}
|
|
183
|
+
return tight(`{\n${members.join('\n')}\n${indent}}`);
|
|
184
|
+
}
|
|
185
|
+
case 'nullable':
|
|
186
|
+
return union([print(def.innerType, indent), tight('null')]);
|
|
187
|
+
case 'lazy': {
|
|
188
|
+
try {
|
|
189
|
+
return print(def.getter(), indent);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// A lazy schema that cannot be forced tells us nothing about its type.
|
|
193
|
+
return tight('any');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
// optional, default, prefault, nonoptional, readonly, catch, success,
|
|
198
|
+
// pipe — every wrapper Zod spells `innerType`, and a pipe's input is the
|
|
199
|
+
// shape a caller writes.
|
|
200
|
+
if (def.innerType)
|
|
201
|
+
return print(def.innerType, indent);
|
|
202
|
+
if (def.in)
|
|
203
|
+
return print(def.in, indent);
|
|
204
|
+
return tight('any');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Whether a property may be omitted.
|
|
209
|
+
*
|
|
210
|
+
* `.default()` counts. `processZodSchema` strips every defaulted field out of
|
|
211
|
+
* the JSON Schema's `required`, because a caller who omits one gets the
|
|
212
|
+
* default rather than a validation error — so the text has to say the same
|
|
213
|
+
* thing the validator does.
|
|
214
|
+
*/
|
|
215
|
+
const isOptional = (node) => {
|
|
216
|
+
const def = node._zod.def;
|
|
217
|
+
if (def.type === 'optional')
|
|
218
|
+
return true;
|
|
219
|
+
if (def.type === 'default' || def.type === 'prefault')
|
|
220
|
+
return true;
|
|
221
|
+
if (def.type === 'nonoptional')
|
|
222
|
+
return false;
|
|
223
|
+
if (def.innerType && isNode(def.innerType))
|
|
224
|
+
return isOptional(def.innerType);
|
|
225
|
+
return false;
|
|
226
|
+
};
|
|
227
|
+
return print(schema, '').text;
|
|
228
|
+
}
|
package/dist/visit.js
CHANGED
|
@@ -31,6 +31,7 @@ import { addWorkflowGraph } from './add/add-workflow-graph.js';
|
|
|
31
31
|
import { addFeature } from './add/add-feature.js';
|
|
32
32
|
import { addAgent } from './add/add-agent.js';
|
|
33
33
|
import { addAnalytics } from './add/add-analytics.js';
|
|
34
|
+
import { addOutgoingWebhook } from './add/add-outgoing-webhook.js';
|
|
34
35
|
import { addAgentScorer } from './add/add-agent-scorer.js';
|
|
35
36
|
import { addApprovalDescription } from './add/add-approval-description.js';
|
|
36
37
|
export const visitSetup = (logger, checker, node, state, options) => {
|
|
@@ -43,6 +44,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
43
44
|
addFileWithFactory(node, checker, state.configFactories, 'CreateConfig');
|
|
44
45
|
addFileWithFactory(node, checker, state.serverLifecycleFactories, 'ServerLifecycle');
|
|
45
46
|
addAnalytics(logger, node, checker, state);
|
|
47
|
+
addOutgoingWebhook(logger, node, checker, state);
|
|
46
48
|
addRPCInvocations(node, state, logger);
|
|
47
49
|
addWireAddon(node, state, logger);
|
|
48
50
|
addWireRemoteAddon(node, state, logger);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.92",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -51,14 +51,13 @@
|
|
|
51
51
|
"ts-json-schema-generator": "2.9.1-next.18",
|
|
52
52
|
"tsx": "^4.23.12",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
54
|
-
"zod": "^4.4.3"
|
|
55
|
-
"zod-to-ts": "^2.1.0"
|
|
54
|
+
"zod": "^4.4.3"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
57
|
+
"@pikku/core": "^0.12.127"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
60
|
+
"@pikku/core": "^0.12.127",
|
|
62
61
|
"@types/node": "^24.13.3"
|
|
63
62
|
},
|
|
64
63
|
"engines": {
|