SnippetBase
The root class for all DSL elements. Provides context plus the optional attribution inputs generatorKey and stackTrail. Both Projections and anonymous Snippets descend from it — via a language…
Source
skmtc/deno/core/dsl/SnippetBase.ts (core, language-blind) and
skmtc/deno/lang-typescript/src/TsSnippet.ts (the TypeScript
snippet base generators actually extend).
Class
// core — language-blind; has NO register
class SnippetBase {
context: GenerateContextType
skipped: boolean = false
generatorKey: GeneratorKey | undefined
stackTrail: StackTrail
constructor(args: {
context: GenerateContextType
generatorKey?: GeneratorKey // attribution (gen-maps) input only
stackTrail?: StackTrail // attribution input only
})
}
// lang-typescript — the authoring base
class TsSnippet extends SnippetBase {
static lang: Lang
register(args: TsRegisterArgs & { destinationPath: string }): void
defineAndRegister<V>(args: TsDefineAndRegisterArgs<V>): TsDefinition<V>
}SnippetBase is language-blind: it has no register and no
language. Register ergonomics live on each language package's
snippet base (TsSnippet.register delegates to that package's
register function) — a raw SnippetBase subclass that tries to
register is a compile-time error. The reason the base is
foundational despite its size: every DSL element ultimately descends
from it, which means context (and the attribution capture wiring)
is universally available.
Constructor
new TsSnippet({
context: GenerateContextType,
generatorKey?: GeneratorKey, // optional attribution input
stackTrail?: StackTrail // optional attribution input; clone at the call site
})Subclasses pass context through super({ context }). The
projection-base factories pass generatorKey as well, derived from
the generator's id, the operation/refName, and the variant.
Snippets built from a schema node pass
stackTrail: schema.stackTrail.clone() so attribution (gen-maps)
can point output back at the source fragment.
You rarely instantiate the bases directly — you subclass TsSnippet
(or extend a projection base built on it).
Properties
context: GenerateContextType
The Generate-phase context. Every Snippet and Projection has access
to it. Used for findDefinition lookups, cross-generator
coordination (in the rare Snippet case), and indirect access to the
file map.
skipped: boolean
A flag indicating the Snippet was filtered out by some upstream
process. Default false. Used by the engine; subclasses rarely
read or write it directly.
generatorKey: GeneratorKey | undefined
The generator key — a branded, pipe-delimited composite of
generatorId, the operation/refName, and the variant. On
SnippetBase it is an optional attribution input only (gen-maps
provenance) — snippet register is keyless. The Driver-side
affirmDefinition integrity check builds its own key from the
projection statics.
Set by the projection-base factory constructors. Anonymous Snippets
typically have undefined here (they don't participate in the
cache directly; their parent Projection does).
stackTrail: StackTrail
Position of the schema fragment this snippet was built from —
attribution input only. Pass a clone
(schema.stackTrail.clone()); the empty trail means "no single
originating node".
Methods (on TsSnippet)
register(args: TsRegisterArgs & { destinationPath: string }): void
Delegates to the lang package's register function
(lang-typescript/src/register.ts), which converts the concise
import form to TsImport objects, creates the destination TsFile
on first write, and hands pure data (ContextRegisterArgs) to the
neutral context.register.
type TsRegisterArgs = {
imports?: Record<string, ImportNameArg[]>
reExports?: Record<string, TsIdentifier[]>
definitions?: (DefinitionBase | undefined)[]
custom?: Stringable
}destinationPath is always explicit on snippets: a snippet has
no file or settings of its own, so the parent passes the target path
through. Own-file defaulting exists only on Projections, in the
projection-base veneers.
Idempotent. Repeated calls with the same payload are safe —
imports merge per module via TsImport.merge, definitions
first-write-wins per declaration slot.
The shape of ImportNameArg:
type ImportNameArg =
| string // 'X' — plain value import
| { [name: string]: string } // { merge: 'lodashMerge' } — rename form
| { name: string; alias?: string; type?: TsEntityType } // full formPlain strings are equivalent to { name: 'X' }. The full form is
needed only for aliased imports or type: 'type' (type-only
imports).
defineAndRegister<V>(args: TsDefineAndRegisterArgs<V>): TsDefinition<V>
Build a TsDefinition from { identifier, value, destinationPath, description?, leadingComment?, noExport? } and register it in one
call.
Extending the snippet base
Two patterns:
Pattern A: extend TsSnippet directly (anonymous Snippet)
class MyFieldSnippet extends TsSnippet {
name: string
label: string | undefined
constructor({
context, name, label, destinationPath
}: { context: GenerateContextType; name: string; label?: string; destinationPath: string }) {
super({ context })
this.name = name
this.label = label
this.register({
imports: { '@/components/fields/string-field': ['StringField'] },
destinationPath
})
}
toString(): string {
return `<StringField name="${this.name}"${this.label ? ` label="${this.label}"` : ''} />`
}
}Notes:
- Constructor takes
destinationPathexplicitly (Snippets don't have their own exportPath). registerruns in the constructor — imports are accumulated beforetoString()is ever called.toString()is pure: derived fromthis.*fields set in the constructor.
Pattern B: extend through a projection base (named Projection)
class MyProjection extends MyGenBase {
// MyGenBase is constructed via toTsOasOperationProjectionBase(...)
// and extends TsSnippet, which extends SnippetBase.
constructor(args: OasOperationProjectionConstructorArgs<MyEnrichmentSchema>) {
super(args)
// ... can call this.register, this.insertOperation, etc.
}
override toString(): string {
return `... ${this.someField} ...`
}
}The projection base wires up settings (identifier + exportPath +
enrichments + variant), the own-file register / registerInto
overrides, and the insertOperation / insertModel /
insertNormalizedModel convenience methods.
The toString() contract
SnippetBase doesn't declare toString() as abstract, but
every meaningful Snippet must override it. Without toString(),
the default Object.prototype.toString returns "[object Object]"
— template-literal interpolation produces garbage output.
Subclasses should:
- Return a string deterministically from
this.*fields - Not mutate
this(toStringmay be called multiple times) - Not perform side effects (register, etc. — those go in the constructor)
- Declare
toStringas a prototype method, never an instance field — every instance self-wraps itstoStringat construction for attribution capture (gen-maps), and an arrow-function field would overwrite the wrapper
Common questions
Why is register on the snippet base rather than on GenerateContext only?
Both Projections and Snippets need to register imports and
definitions, typed by their language's concise vocabulary — which
core can't name. Putting register on the language snippet base
(TsSnippet) gives every DSL element the same primitive without
threading context around, while keeping core's context.register
a pure-data seam. Core's SnippetBase itself has no register.
Should I extend TsSnippet or a projection base?
- Anonymous fragment (e.g., a single field, a JSX element, a
partial expression) → extend
TsSnippetdirectly. - Named, exportable artifact (e.g., a hook, a form component, a
validation schema) → extend a projection base built by
toTsOasOperationProjectionBase/toTsModelProjectionBase/toTsGqlOperationProjectionBase.
The deciding question: does this need its own name and file? If yes, Projection; if no, Snippet. See projections-and-snippets concept.
Can a Snippet have a register call without a toString()?
Technically yes — a Snippet that just registers imports and then isn't embedded anywhere would still get its imports collected. But this is a code smell: a Snippet exists to be embedded. If you're registering imports without producing output, consider whether the parent should just do the registration directly.
Why isn't toString() abstract on SnippetBase?
Historical: making it abstract would force subclasses to declare it explicitly even when inheriting from a parent that already does. The current approach trusts subclasses to override appropriately. A more disciplined version would make it abstract; the choice not to was pragmatic.
What's GeneratorKey?
A branded, pipe-delimited string type — 4 segments for operations
(id|path|method|variant OAS, id|rootKind|fieldName|variant GQL),
3 for models (id|refName|variant), 1 for generator-only. Used by
the Driver's affirmDefinition to verify that a cached Definition
came from the same generator, and by attribution (gen-maps) as a
provenance label. The branding prevents misuse (you can't pass a
plain string where a GeneratorKey is expected).
Anonymous Snippets don't need a generatorKey — they're not
cached directly; on a snippet it is optional attribution input.
Projections get theirs injected by the projection-base factory
constructor.
Stringable
A loose convention: anything with a toString() method that
produces meaningful output is "Stringable." Every SnippetBase
descendant is Stringable. So is anything else with a
toString() (built-in arrays, custom value types, etc.).
The DSL composes via template-literal interpolation, which calls
String() on interpolated values, which falls back to
toString(). So ${anySnippet} just works.
Related types
// What TsSnippet.register accepts (plus destinationPath)
type TsRegisterArgs = {
imports?: Record<string, ImportNameArg[]>
reExports?: Record<string, TsIdentifier[]>
definitions?: (DefinitionBase | undefined)[]
custom?: Stringable
}
// Per-import shape
type ImportNameArg =
| string
| { [name: string]: string }
| { name: string; alias?: string; type?: TsEntityType }
// Generator-key brand
type GeneratorKey = string & { __brand: 'GeneratorKey' }See also
- Projections and Snippets concept — the two-level model
- API: GenerateContext — what
registerforwards to - API: Definition — what
register({ definitions })accepts - API: Identifier — what
reExportsand import names use - API: Projection bases — what extends SnippetBase via the projection layer
skmtc-generatorskill §6 — Snippet scaffold (Pattern A)
Inserted
The return value of insertOperation and insertModel. Carries the peer Projection's ContentSettings and resulting Definition, plus four accessor helpers — most importantly toName(), which is how the…
Entry factories
The three factories that produce a generator's pipeline entry — toOasOperationEntry, toGqlOperationEntry, and toModelEntry. Each takes a config object and returns a Entry value whose type…