How to compose with another generator
Reference another generator's Projection from your generator's output — by name, not by source text.
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
- Both generators installed (the peer can be cloned, JSR-stock, or another local generator).
- Familiarity with cross-generator coordination.
Steps
Import the peer Projection class
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:
import { ZodProjection } from '@local/gen-zod/src/ZodProjection.ts'Call insertOperation or insertNormalizedModel from the constructor
In your Projection's constructor, declare what you need:
// 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:
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
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
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
After regenerating, your file should:
- Have an
import { ... } from '<peer-export-path>'line at the top - Reference the imported name inline
Inspect:
cat src/generated/<your-output-path>.tsThe peer file should also exist with the expected identifier:
cat src/generated/<peer-export-path>.tsBoth 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
A naive composition might say "let me grab the peer's
toString() result and stitch it into my output." This breaks
two ways:
- Order-dependent. If your
toString()runs before the peer's, the peer's output doesn't exist yet. - 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.
Troubleshooting
identifier.nameis undefined — The peer Projection wasn't created. Check the peer'sisSupportedfilter — your call may have been gated out.- Import line not in output — Confirm the
insertNormalizedModel(orinsertModel) 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
There are two related methods:
this.insertNormalizedModel(on the projection base — wraps the context method, auto-fillsdestinationPath)this.context.insertNormalizedModel(onGenerateContextdirectly — caller suppliesdestinationPath)
Both are valid; prefer the projection-base wrapper in generator code.
Related
- API: GenerateContext —
insertModel/insertNormalizedModelreference - Cross-generator coordination concept
- Recipe: composing multi-generator stacks —
the broader walked example using
gen-shadcn-formandgen-shadcn-table - How to swap a peer dependency — the related "I want a different peer" task
- How idempotency works