# How to compose with another generator



## When to use this [#when-to-use-this]

Your generator's output needs to reference what another generator
produces. Common cases: a hook generator referencing a Zod schema,
a form generator referencing a mutation hook, a TypeScript
operation generator referencing a model.

## Prerequisites [#prerequisites]

* Both generators installed (the peer can be cloned, JSR-stock,
  or another local generator).
* Familiarity with [cross-generator coordination](/docs/concepts/cross-generator-coordination).

## Steps [#steps]

### Import the peer Projection class [#import-the-peer-projection-class]

```ts
import { ZodProjection } from '@skmtc/gen-zod'
import { TsProjection } from '@skmtc/gen-typescript'
```

You import the **class** (with its static methods like
`toIdentifierName` and `toExportPath`), not its rendered output. The
class is what the engine uses as a cache key.

If the peer is a sibling clone in your project, import via the
project's local path:

```ts
import { ZodProjection } from '@local/gen-zod/src/ZodProjection.ts'
```

### Call `insertOperation` or `insertNormalizedModel` from the constructor [#call-insertoperation-or-insertnormalizedmodel-from-the-constructor]

In your Projection's constructor, declare what you need:

```ts
// TanstackQueryBase = toTsOasOperationProjectionBase({...}) in your base.ts

class TanstackQuery extends TanstackQueryBase {
  constructor(args) {
    super(args)

    // Get the request-body Zod schema
    const requestBodySchema = this.operation.toRequestBody(({ schema }) => schema)
    if (requestBodySchema) {
      this.requestZod = this.insertNormalizedModel(ZodProjection, {
        schema: requestBodySchema,
        fallbackName: `${toEndpointName(this.operation)}Body`
      })
    }
  }
}
```

`this.insertNormalizedModel` is the canonical entry point for
"materialize this schema as a Zod definition" (or any peer
Projection). It returns the peer's `Definition` — read its name off
`.identifier.name`. (`insertModel` and `insertOperation` instead
return an `Inserted`, whose `.toName()` gives the same string.)

For model-by-refName composition, use `insertModel`:

```ts
const userTs = this.insertModel(TsProjection, 'User')
```

Both `insertModel` and `insertNormalizedModel` exist as
projection-base methods (`this.x`) that wrap the underlying
`GenerateContext` methods (`this.context.x`). The projection-base
versions auto-fill `destinationPath` from `this.settings`.

### Use the returned `Definition` to get the identifier name [#use-the-returned-definition-to-get-the-identifier-name]

```ts
const zodName = this.requestZod.identifier.name
// → e.g., "createUserBody"
```

`identifier.name` is the name string the peer Projection's
`toIdentifierName` produced.

### Reference the name in your template [#reference-the-name-in-your-template]

```ts
override toString(): string {
  return `
    export const useCreateUser = () => useMutation({
      mutationFn: (body) => fetch('/users', {
        method: 'POST',
        body: JSON.stringify(${this.requestZod.identifier.name}.parse(body))
      }).then(r => r.json())
    })
  `
}
```

The import lands automatically — `insertNormalizedModel` records
that your file depends on the peer's file.

## Verification [#verification]

After regenerating, your file should:

1. Have an `import { ... } from '<peer-export-path>'` line at the
   top
2. Reference the imported name inline

Inspect:

```bash
cat src/generated/<your-output-path>.ts
```

The peer file should also exist with the expected identifier:

```bash
cat src/generated/<peer-export-path>.ts
```

Both generators contributed; the engine registered the peer's
definition once even if multiple consumers reference it.

## Why composition is by-name, not by-source-text [#why-composition-is-by-name-not-by-source-text]

A naive composition might say "let me grab the peer's
`toString()` result and stitch it into my output." This breaks
two ways:

1. **Order-dependent.** If your `toString()` runs before the
   peer's, the peer's output doesn't exist yet.
2. **Duplicate registration.** Stitching the source means each
   consumer carries its own copy.

By-name composition sidesteps both. You declare the peer
contribution (via `insertNormalizedModel`), receive the peer's
`Definition`, and reference its `identifier.name` in your template.
The engine handles the file materialization and import injection.

See [how idempotency works](/docs/explanation/how-idempotency-works).

## Troubleshooting [#troubleshooting]

* **`identifier.name` is undefined** — The peer Projection wasn't
  created. Check the peer's `isSupported` filter — your call may
  have been gated out.
* **Import line not in output** — Confirm the
  `insertNormalizedModel` (or `insertModel`) call actually runs.
  It must be reachable from the Projection's constructor.
* **Compile error: "Cannot find module ..."** — Peer generator
  isn't installed in the project. Run `skmtc list <project>` to
  confirm.

## Two ways to call it [#two-ways-to-call-it]

There are two related methods:

* **`this.insertNormalizedModel`** (on the projection base — wraps
  the context method, auto-fills `destinationPath`)
* **`this.context.insertNormalizedModel`** (on `GenerateContext`
  directly — caller supplies `destinationPath`)

Both are valid; prefer the projection-base wrapper in generator
code.

## Related [#related]

* [API: GenerateContext](/docs/reference/api/generate-context) —
  `insertModel` / `insertNormalizedModel` reference
* [Cross-generator coordination concept](/docs/concepts/cross-generator-coordination)
* [Recipe: composing multi-generator stacks](/docs/authoring/recipes/composing-multi-generator-stacks) —
  the broader walked example using `gen-shadcn-form` and
  `gen-shadcn-table`
* [How to swap a peer dependency](/docs/authoring/how-to/swap-a-peer-dependency) —
  the related "I want a different peer" task
* [How idempotency works](/docs/explanation/how-idempotency-works)
