Generate types from the manifest
Your plugin.yaml is the single source of truth for the types your plugin
contributes. This recipe projects it into TypeScript so your behaviors are
typed without hand-written interfaces or casts — run it any time you add or
change a types or nodes entry in the manifest.
It assumes a scaffolded plugin directory (see Scaffold a plugin) and covers the CLI command only. For the manifest schema itself, see Plugin manifest and TypeRef; for what your behaviors do with the result, see Implement a node behavior.
Generate the types
From the plugin's directory, run the CLI directly, or the
gen-typesnpm script the scaffold wires up (libs/plugin-cli/src/commands/scaffold.ts):meridian-plugin gen-types # or: npm run gen-typesYou can also pass a directory explicitly (it defaults to
.):meridian-plugin gen-types path/to/pluginRead the confirmation line (
libs/plugin-cli/src/commands/gen-types.ts,genTypesCommand):@acme/vitals → generated/types.ts, nodes.tsIf your manifest contributes no
typesand no node declares abehavior, there is nothing to project — this is not an error:@acme/vitals: nothing to generate (no contributed types, no behavior nodes).
How dependencies resolve
The dependsOn closure resolves through standard Node resolution:
require.resolve("<dep>/plugin.yaml") from the plugin's own directory — the
dependency must expose ./plugin.yaml in its package.json exports
(libs/plugin-cli/src/discover.ts). When that fails — typically inside this
monorepo, where external-plugins/ sits outside the pnpm workspace and its
plugins are published nowhere — the CLI falls back to the plugin's sibling
directories, matching on the manifest's name (never the folder name). So
in-repo you can run it directly, no linking ritual:
npx tsx libs/plugin-cli/src/cli.ts gen-types external-plugins/posos
A dependency found by neither path is skipped silently; the codegen then errors clearly the moment one of its types is actually referenced, naming the missing dependency.
What gets written
The command writes into a generated/ folder next to plugin.yaml
(libs/plugin-cli/src/codegen.ts, generate):
types.ts— present iffcontributes.typesis non-empty. Oneexport interfaceper contributed type, in manifest order, with each field'sdoccarried over as a JSDoc comment.nodes.ts— present iff at least one node declares abehavior. It exportsPluginNodeIO(node id →{ inputs; outputs }) andBehaviorFor<Id>, aNodeBehaviorFactory<…>alias specialized per node id.
Both files open with a generated-file banner
(// ⚠️ GENERATED by the Clinical Automation plugin CLI (\meridian-plugin gen-types`) — DO NOT EDIT.). Commit generated/` to source control; never hand-edit it — re-run the command
instead.
Field types follow the TypeRef → TypeScript mapping below (full semantics in
TypeRef):
TypeRef.kind |
Generated TypeScript |
|---|---|
primitive |
string, number, or boolean, depending on name |
coded |
a literal union of the value-set's codes, or string if the value-set is external: true |
quantity |
{ value: number; unit: string; system?: string } |
object |
the named interface (imported if owned by a dependsOn plugin) |
ref |
{ id: string } |
list |
T[], recursively mapping the element type |
any |
unknown |
Here is the shape of real output, from the core @posos/clinical plugin
(external-plugins/clinical/generated/):
// generated/types.ts
export interface Observation {
status: "registered" | "preliminary" | "final" | "amended" | "cancelled" | "entered-in-error";
code: CodeableConcept;
value: { value: number; unit: string; system?: string };
effective: string;
interpretation?: CodeableConcept;
}
// generated/nodes.ts
export interface PluginNodeIO {
"trigger.bio-result-received": { inputs: {}; outputs: { "result"?: Observation } };
"patient.append-observation": { inputs: { "result": Observation }; outputs: {} };
// …one entry per node with a `behavior`
}
export type BehaviorFor<Id extends keyof PluginNodeIO> =
NodeBehaviorFactory<PluginNodeIO[Id]["inputs"], PluginNodeIO[Id]["outputs"]>;
Wire behaviors to the generated types
Switch a behavior from the hand-written generic form to the generated alias and drop the inline shapes:
// before gen-types (or without it — both forms compile)
import type { NodeBehaviorFactory } from "@meridian/shared/plugin/behavior";
export const exampleScore: NodeBehaviorFactory<
{ value: number },
{ result?: { score: number; band: "low" | "high" } }
> = () => ({ /* … */ });
// after gen-types
import type { BehaviorFor } from "../generated/nodes.js";
export const exampleScore: BehaviorFor<"example.score"> = () => ({ /* … */ });
Both are equivalent — BehaviorFor<Id> is a convenience, not a requirement.
generated/nodes.ts also re-exports the port shapes as plain interfaces
through PluginNodeIO, in case you need them outside a behavior factory.
Resolve types across dependsOn plugins
An object field can reference a type owned by a plugin listed in your
dependsOn. Resolution walks the transitive dependsOn closure through
standard Node resolution, from your plugin's own node_modules
(libs/plugin-cli/src/discover.ts, loadPluginClosure):
For each
dependsOnentry, the CLI resolves<dep>/plugin.yaml— this requires the dependency to be an installed npm package whosepackage.jsonexports"./plugin.yaml"(and, for the TypeScript import to resolve,"./types"), the same pattern used by the repository's own plugins:"exports": { "./types": "./generated/types.ts", "./plugin.yaml": "./plugin.yaml" }A
dependsOnentry that isn't installed is skipped silently at this stage — resolution only fails loudly if one of its types is actually referenced.When a referenced type is found on a dependency, the generated import comes from that package's
/typessub-path rather than being redeclared locally:import type { CodeableConcept } from "@posos/common/types";Referencing a type outside the closure is a build-time error (
libs/plugin-cli/src/codegen.ts,mapRef):✗ [@acme/vitals] type "Observation" not found in the dependsOn closure (@acme/vitals). Declare the missing dependency.Fix it by adding the owning plugin under
dependsOninplugin.yamlandnpm installing it, then re-runninggen-types.
Regenerate after every manifest change
gen-types is not watched or run automatically — re-run it whenever you add,
rename, or retype a types entry, add or change a node's inputs/outputs,
or add/remove a node's behavior. There is no incremental mode: each run
rewrites both files from the current manifest.
Working inside the platform repository instead of a standalone package? The same codegen core regenerates every plugin under
external-plugins/at once viapnpm --filter @meridian/api run gen:types(apps/api/scripts/gen-plugin-types.ts). Third-party authors only ever use the per-pluginmeridian-plugin gen-typesshown above. See Codegen internals for how the two share one implementation.
Related
- Plugin manifest — the full
plugin.yamlschema. - TypeRef — every
TypeRef.kindand its generated TypeScript. - Implement a node behavior — writing
run/propose/fromEventagainstBehaviorFor<Id>. - Scaffold a plugin — where
plugin.yamland thegen-typesscript come from. - Validate a workflow — the next check after your types compile.
- Build and bundle —
generated/is compiled like any other source file when you shipdist/. - Codegen internals — how the shared codegen core works under the hood.
- Build a plugin, end to end — this step in context.