@zapier/kitcore 0.4.0 → 0.5.1

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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # @zapier/kitcore
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c398f14: A caller-provided parameter is now used exactly as given: `controller.resolve()` and `controller.start()` settle seeded input up front, so the resolution walk no longer descends into provided objects or arrays and only asks for parameters the caller left out.
8
+
9
+ ## 0.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5f5f028: The module model is complete; the legacy function-plugin bridge is deprecated.
14
+
15
+ New:
16
+ - `createSdk(root, { configuration })` injects immutable values by plugin id
17
+ at build time (your SDK's options, framework options). A `declareProperty`
18
+ or `declareOptionalProperty` (new) stand-in with a matching id receives the
19
+ value as a normal import.
20
+ - The `kitcore/coreOptions` configuration entry now accepts a
21
+ `logDeprecation` handler, called on every invocation of a deprecated
22
+ method. The default logs a warning once per process.
23
+ - `defineHook`, a new plugin kind that attaches behavior to methods:
24
+ `observe` runs `onMethodStart` / `onMethodEnd` observers over every method
25
+ call, and `wrap` is targeted, contract-preserving middleware over a direct
26
+ import (a `{ imports, input, state, next }` bag).
27
+ - Every plugin can declare `dispose` beside `setup`. `disposeSdk(sdk,
28
+ input?)` runs them in reverse build order — call it to release what your
29
+ plugins acquired (timers, listeners). Idempotent; failures aggregate into
30
+ `CoreDisposeError`.
31
+ - `resolvePlugin(sdk, ref)` reads a plugin binding off a built sdk, for
32
+ infrastructure code that can't declare `imports`.
33
+ - `PluginSurface<typeof plugin>` derives the surface a plugin contributes
34
+ (method callable, property value, or an aggregate's bindings), so you don't
35
+ hand-write provides types. The `PluginSummary` / `LeafSummary` ledger types
36
+ are exported alongside it, so a package that exports plugins can emit their
37
+ inferred descriptor types — completeness ledger included — in its
38
+ declarations.
39
+ - Raw-output methods now get the full method boundary — input validation
40
+ branded through your `adaptError`, lifecycle hooks — while staying
41
+ synchronous.
42
+ - Object parameters resolve more naturally in interactive mode: an optional
43
+ object sits behind one entry question (declining skips its field fetch),
44
+ required fields come first, and the remaining optionals are offered as a
45
+ batch. A nested field's `requireParameters` can now name top-level
46
+ parameters. `collection` questions carry `container: "array" | "object"`;
47
+ hosts that render `message` + `actions` generically need no changes.
48
+
49
+ Breaking:
50
+ - `definePlugin`'s `middleware` map is removed. Declare a `defineHook` with
51
+ `wrap` instead.
52
+ - `listItems` is required on dynamic resolvers: a dynamic resolver IS a
53
+ candidate-lister. For a free-text field, use `type: "static"`.
54
+ - On `collection` questions, `count` and `min` are now optional (present on
55
+ array decisions only, absent on the new object gates). A host reading
56
+ `question.count` unconditionally must handle `undefined`.
57
+
58
+ Deprecated (runtime warnings now, removal in a later release):
59
+ `createPluginStack`, `createPluginMethod`, `createPaginatedPluginMethod`,
60
+ `createCorePlugin`, `fromFunctionPlugin`, `defineLegacyMerge`, and the
61
+ function form of `definePlugin`. Author with `defineMethod` /
62
+ `defineProperty` / `definePlugin({ ... })` and build with `createSdk`.
63
+
64
+ Fixed: an sdk built by one bundled copy of kitcore is now readable by another
65
+ copy's `getContext` / `resolvePlugin` (the context key moved to the global
66
+ symbol registry).
67
+
3
68
  ## 0.4.0
4
69
 
5
70
  ### Minor Changes
package/README.md CHANGED
@@ -40,10 +40,11 @@ sdk.greet({ name: "Ada" }); // "Hi Ada"
40
40
 
41
41
  ## Plugins are modules
42
42
 
43
- There are three kinds of plugin, and an SDK is just a materialized plugin:
43
+ There are four kinds of plugin, and an SDK is just a materialized plugin:
44
44
 
45
45
  - **Method** (`defineMethod`) — a leaf that _is_ a function.
46
46
  - **Property** (`defineProperty`) — a leaf that _is_ a value.
47
+ - **Hook** (`defineHook`) — a leaf contributing cross-cutting behavior (see [Hooks](#hooks-wrap-and-observe)).
47
48
  - **Aggregate** (`definePlugin`) — re-exports other plugins under binding names.
48
49
 
49
50
  A plugin declares `imports` (an array of the plugins it depends on) and receives them as a flat `imports` bag, with each import bound under its own name. The two ends share the word the way ES modules do: `import { x } from "y"` is the declaration, `x` is the binding.
@@ -211,15 +212,19 @@ const fetchMethod = defineMethod({
211
212
  sdk.fetch("https://example.com", { method: "GET" });
212
213
  ```
213
214
 
214
- ## Middleware
215
+ ## Hooks: wrap and observe
215
216
 
216
- An aggregate can wrap the methods it imports. A `middleware` entry is keyed by the import binding it wraps and receives `{ imports, next, input }`. It must preserve the target's contract (enforced by the types), so it cannot change the public signature.
217
+ Cross-cutting behavior lives in a **hook** (`defineHook`), a fourth kind of leaf with two faces.
218
+
219
+ `wrap` is targeted middleware. An entry is keyed by the import binding it wraps and receives `{ imports, next, input }`. It must preserve the target's contract (enforced by the types), so it cannot change the public signature.
217
220
 
218
221
  ```ts
219
- const auth = definePlugin({
222
+ import { defineHook } from "@zapier/kitcore";
223
+
224
+ const auth = defineHook({
220
225
  name: "auth",
221
226
  imports: [transport, credentials],
222
- middleware: {
227
+ wrap: {
223
228
  transport: ({ imports, next, input }) =>
224
229
  next({
225
230
  ...input,
@@ -228,34 +233,55 @@ const auth = definePlugin({
228
233
  },
229
234
  });
230
235
 
231
- const retry = definePlugin({
236
+ const retry = defineHook({
232
237
  name: "retry",
233
238
  imports: [transport, auth], // import auth so retry nests around it
234
- middleware: { transport: ({ next, input }) => withRetry(() => next(input)) },
239
+ wrap: { transport: ({ next, input }) => withRetry(() => next(input)) },
235
240
  });
236
241
  ```
237
242
 
238
243
  A method that imports `transport` knows nothing about `auth` or `retry`; it just calls `imports.transport(...)` and the chain runs automatically. Nesting follows the dependency edges (`retry` imports `auth`, so it wraps around it), not registration order, and `next(input)` runs the next layer.
239
244
 
245
+ `observe` is fire-and-forget: `onMethodStart` / `onMethodEnd` observers that the method boundary fires around every SDK method (`input` carries the lifecycle context: `methodName`, `args`, `durationMs`, `error`, ...). Observers run defensively, so a throwing observer never breaks the observed call, and an observer that itself calls SDK methods does not re-trigger itself. `setup` gives the hook private state, delivered to each observer.
246
+
247
+ ```ts
248
+ const telemetry = defineHook({
249
+ name: "telemetry",
250
+ setup: () => ({ events: [] as string[] }),
251
+ observe: {
252
+ onMethodEnd: ({ input, state }) =>
253
+ state.events.push(`${input.methodName}:${input.error ? "err" : "ok"}`),
254
+ },
255
+ });
256
+ ```
257
+
258
+ A hook joins the graph like any other plugin: import or export it from an aggregate.
259
+
240
260
  ## Dependency injection
241
261
 
242
262
  A plugin "names what it needs and receives it" without knowing who supplied it. That makes test doubles and configured providers trivial: register a real implementation under the same id and dependents get it transparently. This is the normal way one plugin reaches another, import the plugin, read it from the `imports` bag.
243
263
 
244
- ### The context escape hatch (avoid)
264
+ ### Configuration
245
265
 
246
- There is a built-in `dangerousContextPlugin` that hands you the SDK's raw internal context (the live plugin graph plus legacy compatibility fields):
266
+ Runtime values (an options object, a configured client) enter through `createSdk`'s `configuration` map, keyed by plugin id. Each entry materializes as a value property with that id, satisfying a `declareProperty` / `declareOptionalProperty` stand-in so plugins read configuration through `imports` like any other dependency, and tests inject doubles the same way.
247
267
 
248
268
  ```ts
249
- import { dangerousContextPlugin } from "@zapier/kitcore";
269
+ import { declareOptionalProperty } from "@zapier/kitcore";
250
270
 
251
- const whoami = defineMethod({
252
- name: "whoami",
253
- imports: [dangerousContextPlugin],
254
- run: ({ imports }) => Object.keys(imports.context.plugins),
271
+ const configRef = declareOptionalProperty<"config", { pageSize?: number }>({
272
+ id: "config",
273
+ });
274
+
275
+ const listThings = defineMethod({
276
+ name: "listThings",
277
+ imports: [configRef],
278
+ run: ({ imports }) => api.list({ pageSize: imports.config?.pageSize ?? 10 }),
255
279
  });
256
- ```
257
280
 
258
- **Steer clear of it.** The context shape is an internal implementation detail and may change without notice, so anything built on it is fragile. Import the specific plugins you need instead, and use `getRegistryPlugin` (below) for surface introspection. The `dangerous` prefix is there to make the risk loud at every import site; it exists mainly so a not-yet-migrated plugin can reach legacy state during a transition.
281
+ const sdk = createSdk(listThings, {
282
+ configuration: { config: { pageSize: 50 } },
283
+ });
284
+ ```
259
285
 
260
286
  ## Extending a built SDK
261
287
 
@@ -268,6 +294,24 @@ addPlugin(sdk, defineMethod({ name: "ping", run: () => "pong" }));
268
294
  sdk.ping(); // "pong", typed
269
295
  ```
270
296
 
297
+ ## Teardown
298
+
299
+ `dispose` is `setup`'s dual: a leaf that owns a resource declares one, receiving `{ imports, state, input }`. `disposeSdk(sdk, input?)` runs every disposer in reverse build order (dependents before their dependencies) and is idempotent; `input` is forwarded to each disposer (e.g. `{ exitCode }` from a CLI shutdown).
300
+
301
+ ```ts
302
+ import { disposeSdk } from "@zapier/kitcore";
303
+
304
+ const emitter = defineProperty({
305
+ name: "emitter",
306
+ setup: () => createEmitter(),
307
+ get: ({ state }) => state,
308
+ dispose: ({ state }) => state.close(),
309
+ });
310
+
311
+ // at shutdown
312
+ await disposeSdk(sdk, { exitCode: 0 });
313
+ ```
314
+
271
315
  ## Introspection
272
316
 
273
317
  Re-export the built-in `getRegistryPlugin` to put a `getRegistry()` method on the SDK. It reports the live surface as plain data (one entry per binding, with the leaf's metadata), which is what drives generated docs, CLI commands, and MCP tools.