# Projections and Snippets



A Projection produces an `export const X = …` declaration. A Snippet produces a
fragment that gets spliced into a Projection's body via template-literal
interpolation. The split makes file-scope identity a deliberate choice, not a
default.

## The one-line distinction [#the-one-line-distinction]

A Projection has a name and a file. A Snippet has neither.

Projections produce `export const NAME = VALUE;` (or `export type NAME = …;`)
declarations. They're addressable, importable, and participate in
cross-generator coordination through the `(name, exportPath)` cache. Snippets
produce JSX elements, function bodies, type fragments, or any other
stringifiable value that gets *embedded into* a Projection.

## Why two levels? [#why-two-levels]

The split maps onto JSX components vs JSX expressions. Top-level React
components have names, get imported by other modules, and exist in their own
files. JSX expressions (`<Button onClick={...} />`) are anonymous and embedded
in their parent's rendering. SKMTC's DSL makes the same cut.

The alternative — a single "unit" abstraction — would force every snippet to
have a name and a path, even ones that exist only as an inline `<StringField />`
JSX element. That's overhead with no benefit to the cross-generator coordination
story (which only needs file-scope items in its cache). The two-level split
keeps the cache small and the authoring ergonomics natural.

## Projections [#projections]

The full-citizen class for unit-of-output:

* Extends a projection base *built by a factory*: the language veneer's
  `toTsModelProjectionBase(config)`, `toTsOasOperationProjectionBase(config)`,
  or `toTsGqlOperationProjectionBase(config)` (from `@skmtc/lang-typescript`)
  returns the class the generator subclasses. Core's underlying factories
  (`toModelProjectionBase(base, config)` and siblings) do the assembly.
* Has a class-level `id` (the generator package name)
* Has static methods on the class: `toIdentifierName`, `toIdentifierType`,
  `toExportPath`, `toEnrichments`, `toEnrichmentSchema`
* Has instance properties: `settings: ContentSettings` (= identifier +
  exportPath + enrichments), plus `context` and `generatorKey`
* Has instance methods inherited from the base: `insertOperation`,
  `insertModel`, `insertNormalizedModel` (these auto-fill `destinationPath` from
  `this.settings.exportPath`)

Drivers wrap a Projection's output value in a `Definition`, which produces the
`export const NAME = VALUE;` statement. The Projection's `toString()` produces
just the VALUE; the wrapping is automatic.

### Examples in stock generators [#examples-in-stock-generators]

* `ZodProjection` (`gen-zod`) — produces
  `export const userBody = z.object({...})`
* `TsProjection` (`gen-typescript`) — produces `export type UserBody = { ... }`
* `ShadcnForm` (`gen-shadcn-form`) — produces
  `export const CreateUserForm = (props) => ...`
* `TanstackQuery` (`gen-tanstack-query-fetch-zod`) — produces
  `export const useCreateUser = (args) => ...`

Each is a class that extends an operation or model projection base. Each has the
static `toIdentifierName` / `toExportPath` pair that makes it addressable in the
cache.

### The three projection-base factories [#the-three-projection-base-factories]

| Factory (lang veneer)            | Source unit                           | When                                             |
| -------------------------------- | ------------------------------------- | ------------------------------------------------ |
| `toTsModelProjectionBase`        | An OAS schema component (a `refName`) | Generators that produce one file per type/schema |
| `toTsOasOperationProjectionBase` | An OAS operation (path + method)      | Generators that produce one file per endpoint    |
| `toTsGqlOperationProjectionBase` | A GraphQL operation                   | GraphQL-side generators                          |

Each factory takes the generator's config (`id`, `toIdentifierName`,
`toIdentifierType`, `toExportPath`, `toEnrichmentSchema`, …) and returns the
class to extend — there is no subclassable `ModelProjectionBase` class to
import.

Each base provides the `insertOperation` / `insertModel` /
`insertNormalizedModel` methods with `destinationPath` auto-filled from
`this.settings.exportPath` — a convenience over calling the underlying methods
on `context` directly.

## Snippets [#snippets]

The anonymous fragment class:

* Extends the language's snippet base directly (`TsSnippet` from
  `@skmtc/lang-typescript`, which carries `register`) — or core's language-blind
  `SnippetBase` when it never registers anything
* No required static methods
* No `settings` property
* Receives constructor arguments like `destinationPath` from the parent that
  will embed it
* Embedded into a parent via `${this.snippet}` template-literal interpolation
* The parent calls `toString()` on the snippet implicitly via the template
  literal

### Examples in stock generators [#examples-in-stock-generators-1]

* `FormFields` (`gen-shadcn-form`) — a list of field renderings inside a form
* `StringInput`, `SelectInput`, `CheckboxInput`, etc. — individual JSX field
  elements
* `CustomValue` — escape hatch wrapping arbitrary TS fragments that don't fit
  the OAS-derived schema model
* `Identifier` — a name with entity-type tracking (technically a Snippet, though
  rarely thought of that way)
* `Definition` — yes, even Definition is technically a Snippet (see below)

## How Definition bridges them [#how-definition-bridges-them]

`Definition` extends `SnippetBase` but plays a special bridging role: it's the
wrapper that makes a Projection's value exportable.

```
Projection.toString()
       ↓ produces VALUE (e.g., "z.object({...})")
Definition.toString()
       ↓ produces "export const NAME = VALUE;\n"
File.toString()
       ↓ produces the complete file with imports + definitions
```

So `Definition` is the bridge between "unit of output" (Projection) and
"rendered TypeScript declaration." It's a Snippet in the sense that it extends
`SnippetBase`, but its role is specifically projection-wrapping. You rarely
construct one directly — Drivers do.

## The composition model [#the-composition-model]

JavaScript template literals call `String()` on interpolated values, which falls
back to `.toString()`. Every `SnippetBase` descendant implements `toString()`,
so they compose naturally:

```ts
class CreateUserForm extends ShadcnFormBase {
  fields: FormFields; // ← a Snippet
  clientName: string;

  constructor(args) {
    super(args);
    this.fields = new FormFields(args);
    this.clientName = this.insertOperation(TanstackQuery, args.operation)
      .toName();
  }

  override toString() {
    return `(props: CreateUserFormProps) => {
      const form = useForm({...})
      const mutator = ${this.clientName}()
      return (
        <Form {...form}>
          <form onSubmit={...}>
            ${this.fields}        // ← Snippet's toString() embedded here
            <Button>Submit</Button>
          </form>
        </Form>
      )
    }`;
  }
}
```

The interpolation pattern is the entire composition mechanism. No registry, no
slot system, no callback API. Anything that has a `toString()` (which is
everything that extends `SnippetBase`) can be embedded.

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

* **Other generators might reference it by name** → Projection
* **It needs a file-scope export (`export const X = ...`)** → Projection
* **It's a fragment inside another generator's output** → Snippet
* **Unsure** → Probably Snippet. Promote to Projection only when cross-file
  identity is needed.

The cost asymmetry: turning a Snippet into a Projection later is simple
(subclass a base, add static methods). Turning a Projection into a Snippet is
also simple but removes it from the cross-generator coordination cache. Default
to Snippet unless you have a reason.

## Choosing the right primitive — mechanical traps to avoid [#choosing-the-right-primitive--mechanical-traps-to-avoid]

Three shapes that look reasonable but break the framework's invariants in
specific ways. Each has the same root: reaching for an ad-hoc construction when
a Projection or `SnippetBase` descendant fits.

### File-scope export via a Snippet + `defineAndRegister` [#file-scope-export-via-a-snippet--defineandregister]

```ts
// ❌ A supplemental file-scope type, registered as a sibling Definition
// in the parent's exportPath, wrapping a Snippet instead of a Projection
class FormValuesSnippet extends SnippetBase {
  override toString() {
    return `{ firstName: string; email: string }`;
  }
}

defineAndRegister(context, {
  identifier: createType(`${name}Values`),
  value: new FormValuesSnippet({ context }),
  destinationPath: settings.exportPath,
});
```

This *does* land an `export type CreateCustomerFormValues = {...}` line in the
File. It also fails on three framework guarantees:

| Guarantee                                                                              | Why it fails                                                                                                                                                                                                                                                                         |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Cache-key identity is `(Producer.toIdentifierName(op), Producer.toExportPath(op))`** | A Snippet has no static `toIdentifierName` / `toExportPath`. The Definition is keyed by whatever name string the caller built. `context.findDefinition({ name, exportPath })` works only for callers who know the exact name string — there's no Producer class to drive the lookup. |
| **`insertOperation(Producer, op)` requires a Projection class as the first arg**       | There is no class to pass. The Definition is unreachable through the operation-reference protocol; future generators that want this same type must re-derive the name string at their own call site.                                                                                 |
| **Rename safety**                                                                      | The identifier name lives at the caller (`${name}Values`). Renaming the convention means editing every caller plus every consumer reading by that name. A Projection's `toIdentifierName` is one site.                                                                               |

Fix: extend the appropriate projection base.
`class FormValuesType
extends MyFormBase { static override toIdentifierName(...) {...} override
toString() {...} }`,
then `insertOperation(FormValuesType, op)`. The mechanics now match the rest of
cross-generator composition.

### Ad-hoc `{ toString: () => '…' }` returned from a helper function [#ad-hoc--tostring-----returned-from-a-helper-function]

```ts
// ❌ A helper that returns a duck-typed Stringable
const renderRow = (cols: number, inner: string) => ({
  toString: () =>
    `<div className="grid gap-4 sm:grid-cols-${cols}">${inner}</div>`,
});
```

This satisfies `Stringable` structurally and renders correctly when
interpolated. It also lacks every framework affordance a real Snippet provides:

| Affordance                                                                    | Why it fails                                                                                                                                                                                                                                           |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`context` access (and therefore `register({ imports, destinationPath })`)** | The object has no `context`. If the markup ever needs to import a peer component (a UI library `<Row>`, a hand-written helper), the import has to bubble up to whichever caller has `context` — coupling the caller to the helper's internals.         |
| **`generatorKey`**                                                            | The integrity machinery (`affirmDefinition`, `findDefinition`'s mismatch detection) operates on values carrying a `generatorKey`. The duck-typed object has none, so it's invisible to the integrity layer if it ever ends up wrapped in a Definition. |
| **`instanceof SnippetBase`**                                                  | Generic code that operates on "SnippetBase or its descendants" can't treat the duck-typed object as a member of the family.                                                                                                                            |

Fix: a real snippet — extend the language's snippet base (`TsSnippet` from
`@skmtc/lang-typescript`, which carries `register`) with its `register` calls
in the constructor.

```ts fragment
import { TsSnippet } from "@skmtc/lang-typescript";

class FormRow extends TsSnippet {
  constructor({ context, cols, inner, destinationPath }) {
    super({ context });
    this.cols = cols;
    this.inner = inner;
    // any imports the markup needs go here
  }
  override toString() {
    return `<div className="grid gap-4 sm:grid-cols-${this.cols}">${this.inner}</div>`;
  }
}
```

Same line count once the class is written; every framework guarantee restored.

### Snippet over-parameterized with sole-caller hardcoded values [#snippet-over-parameterized-with-sole-caller-hardcoded-values]

This one is *not* about the Projection-vs-Snippet choice — the Snippet is the
right primitive, but its constructor signature is wider than necessary:

```ts
// ❌ Five constructor args; four are hardcoded by the sole caller
class FormFooter extends SnippetBase {
  constructor({
    context, destinationPath,
    formIdVar,        // always 'formId'
    isSavingVar,      // always 'isSaving'
    cancelHandlerExpr,// always 'props.onCancel'
    submitLabelExpr   // genuinely varies
  }) { ... }
}
```

Mechanically: every parameter that all callers pass identically still gets typed
in the signature, passed at every call site, closed over by the constructor, and
read by `toString()`. The cost is in the type surface and the call-site
verbosity — not in framework invariants.

Diagnostic question per parameter: &#x2A;would a hypothetical second caller pass a
different value here?* If no → inline the value in the Snippet, remove the
parameter.

This is adjacent to the two anti-patterns above (each is about reaching for the
wrong shape) but the failure mode is API-design debt, not framework breakage.
Listed here because the same triage question — "is this the right shape?" —
catches all three.

## Snippets need destinationPath passed in [#snippets-need-destinationpath-passed-in]

Because Snippets don't have their own `exportPath`, they can't register imports
against their own file — they need to know where their parent is going to land.
The convention: the parent passes `destinationPath` as a constructor argument.

```ts fragment
import { TsSnippet } from "@skmtc/lang-typescript";

class StringInput extends TsSnippet {
  constructor({ context, name, destinationPath }) {
    super({ context });
    this.name = name;

    // Register against the parent's destination file
    this.register({
      imports: { "@/components/fields/string-field": ["StringField"] },
      destinationPath,
    });
  }

  override toString() {
    return `<StringField name="${this.name}" />`;
  }
}
```

The parent Projection has its own `exportPath` from `this.settings.exportPath`,
so it doesn't need this dance. Snippets nested inside a Projection (or another
Snippet) follow the chain — each parent passes `destinationPath` down to its
children.

This is the "side-effect on context with explicit destination" pattern: Snippets
register their imports immediately during construction, before the parent's
`toString()` even runs. By the time `File.toString()` assembles the final
output, all imports across all nested Snippets have already accumulated in the
file's import map.

## Common questions [#common-questions]

### Can a Snippet reach other generators? [#can-a-snippet-reach-other-generators]

Indirectly. A Snippet inside a Projection can call
`this.context.insertOperation(...)` or `this.context.insertNormalizedModel(...)`
directly (Snippets don't have the projection-base wrappers, so they use the
methods on `context`). The returned `Inserted<V, E>` gives access to the peer's
identifier name just like in a Projection.

That said: most cross-generator coordination *happens* in Projection
constructors, where the projection-base wrappers auto-fill `destinationPath`.
Snippets that compose generators are unusual but not forbidden.

### Why isn't `register` on Projections only? [#why-isnt-register-on-projections-only]

`register` lives on the language's snippet base — `TsSnippet` in
`@skmtc/lang-typescript` — because both layers need it and both are built on
that base: Snippets register imports for themselves (against the parent's
file), and Projections (whose bases are built on `TsSnippet` by the veneer
factories) register imports and definitions for their target file. Core's
`SnippetBase` is language-blind and has **no** `register` — register
ergonomics are typed by each language's concise vocabulary, which core can't
name. The same primitive serves both layers — only `destinationPath` differs.

### Is `Definition` really a Snippet? [#is-definition-really-a-snippet]

By inheritance, yes. By role, no — it's a bridging wrapper that Drivers create
automatically. The fact that `Definition extends
SnippetBase` is more of a
technical detail than a conceptual claim. In practice, you compose Projections
(which become Definitions via Drivers) with anonymous Snippets, and you never
touch `Definition` directly.

### Can I have multiple Projections in one file? [#can-i-have-multiple-projections-in-one-file]

Yes. Each Projection has its own `Definition`; the file holds a
definitions map keyed by the identifier's declaration slot. Multiple
Projections targeting the same `exportPath` add their entries to the same
file's `definitions` map. The file's `toString()` joins them with blank lines.

### What if I want a named export but not a full Projection? [#what-if-i-want-a-named-export-but-not-a-full-projection]

Use `register({ definitions: [new TsDefinition({...})] })` directly. This is the
escape hatch — you produce a Definition without going through a Projection base.
The trade-off: no cross-generator coordination (no cache, no integrity check)
for that definition.

Useful when producing boilerplate that doesn't need to be addressable from other
generators (e.g., a constants table, a default-values object).

### Can a Snippet contain another Snippet? [#can-a-snippet-contain-another-snippet]

Yes — composition is unlimited depth. A `FormFields` snippet contains multiple
`StringInput` / `SelectInput` snippets. Each child Snippet embeds itself via
`${this.fields}` in the parent's `toString()`. Imports propagate up via shared
`destinationPath`; the file's import map accumulates contributions from every
nested snippet.

## Further reading [#further-reading]

* [How generators produce output](/docs/concepts/how-generators-produce-output) — who
  instantiates Projections and when; the pull-based model
* [Composing output with Stringable](/docs/concepts/stringable-composition) — how Projection
  and Snippet `toString()` methods compose into rendered output
* [Cross-generator coordination](/docs/concepts/cross-generator-coordination) — how
  Projections find each other
* [The three phases](/docs/concepts/the-three-phases) — where Projections and Snippets fit
  in Parse / Generate / Render
* [API reference: projection-bases](/docs/reference/api/projection-bases)
* [API reference: dsl-snippet-base](/docs/reference/api/dsl-snippet-base)
* [API reference: dsl-definition](/docs/reference/api/dsl-definition)
* [Anatomy of a generator](/docs/authoring/anatomy-of-a-generator) — the
  authoring orientation
