Documentation / Développement / Guides pratiques / Generate types from the manifest

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

  1. From the plugin's directory, run the CLI directly, or the gen-types npm script the scaffold wires up (libs/plugin-cli/src/commands/scaffold.ts):

    meridian-plugin gen-types          # or: npm run gen-types
    

    You can also pass a directory explicitly (it defaults to .):

    meridian-plugin gen-types path/to/plugin
    
  2. Read the confirmation line (libs/plugin-cli/src/commands/gen-types.ts, genTypesCommand):

    @acme/vitals → generated/types.ts, nodes.ts
    

    If your manifest contributes no types and no node declares a behavior, 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 iff contributes.types is non-empty. One export interface per contributed type, in manifest order, with each field's doc carried over as a JSDoc comment.
  • nodes.ts — present iff at least one node declares a behavior. It exports PluginNodeIO (node id → { inputs; outputs }) and BehaviorFor<Id>, a NodeBehaviorFactory<…> 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):

  1. For each dependsOn entry, the CLI resolves <dep>/plugin.yaml — this requires the dependency to be an installed npm package whose package.json exports "./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"
    }
    
  2. A dependsOn entry that isn't installed is skipped silently at this stage — resolution only fails loudly if one of its types is actually referenced.

  3. When a referenced type is found on a dependency, the generated import comes from that package's /types sub-path rather than being redeclared locally:

    import type { CodeableConcept } from "@posos/common/types";
    
  4. 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 dependsOn in plugin.yaml and npm installing it, then re-running gen-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 via pnpm --filter @meridian/api run gen:types (apps/api/scripts/gen-plugin-types.ts). Third-party authors only ever use the per-plugin meridian-plugin gen-types shown above. See Codegen internals for how the two share one implementation.

75 documents7 sectionssource : /docs · généré au build