ContentSettings
The per-Projection settings object: identifier, exportPath, enrichments, and variant. Computed by Drivers from a Projection's static methods (toIdentifierName / toIdentifierType, toExportPath,…
ContentSettings is the smallest yet most load-bearing class in the
DSL. It's the only thing connecting a Projection instance to its
identifier (name + entity type), its export path, and its enrichments.
Drivers compute it before construction; Projections consume it during
toString().
Source
skmtc/deno/core/dsl/ContentSettings.ts
Constructor
class ContentSettings<E = undefined> {
identifier: IdentifierBase
exportPath: string
enrichments: E
variant: string
constructor(args: {
identifier: IdentifierBase
exportPath: string
enrichments: E
variant: string
})
static empty(args: {
identifier: IdentifierBase
exportPath: string
variant?: string // defaults to 'main'
}): ContentSettings<undefined>
}new ContentSettings({
identifier: IdentifierBase, // the assembled identifier (name + lang-typed parts)
exportPath: string, // the file path this Projection lands in
enrichments: E, // the validated enrichment payload (or undefined)
variant: string // 'main' for variants-unaware Projections
})The generic parameter E is the enrichment shape — declared by the
generator's toEnrichmentSchema factory. When the generator declares
no enrichments, E = undefined.
Properties
identifier
The Projection's name and entity-type marker. Used in two ways:
- In the rendered output — wrapped by
Definition, this becomes theexport const NAME =orexport type Name =declaration. - In the cross-generator coordination cache — the
(identifier.name, exportPath)pair is the cache key. Two generators producing the same name in the same file converge on one entry.
See API: Identifier for the entity-type
semantics (TsEntityType:
'variable' | 'type' | 'class' | 'interface' | 'namespace'; the
rendered declaration keyword for 'variable' is const) and the
factory functions.
exportPath
The file path where this Projection's Definition will live in the
output. Computed by the Projection's static toExportPath() method,
typically:
static toExportPath({ operation }): string {
return `${basePath}/forms/${toEndpointName(operation)}.tsx`
}The path is the second half of the cache key — two Projections with
the same identifier.name but different exportPath are distinct
entries (no collision).
The Driver also uses exportPath to route the Projection's
register({ definitions }) call to the right File.
enrichments
The validated enrichment payload for this specific Projection instance. The projection-base factory routes via a key path that depends on the factory kind (OAS operation, model, or GraphQL operation) — see enrichments-shape reference for the three routing structures.
The result is type-narrowed to E, the schema declared by the
generator's toEnrichmentSchema factory. May be undefined when:
- The generator declared no enrichments (
E = undefined) - The user didn't supply enrichments for this operation
- The enrichments are optional and absent
The Projection's toString() reads this.settings.enrichments?.X to
access user-supplied overrides.
Factory
ContentSettings.empty(args)
A convenience factory for the no-enrichments case. Equivalent to:
new ContentSettings({
identifier,
exportPath,
enrichments: undefined
})Used by Drivers when constructing settings for generators that don't declare an enrichment schema. The static method makes the intent explicit: "no enrichments here, just the identifier and path."
const settings = ContentSettings.empty({
identifier: createVariable('userBody'),
exportPath: '@/types/userBody.generated.ts'
})
// → ContentSettings<undefined>Use sites
In Projection constructors (via this.settings)
The Driver constructs ContentSettings and passes it to the
Projection constructor:
const settings = new ContentSettings({
identifier: lang.toIdentifier({
name: projection.toIdentifierName({ operation, enrichments, variant }),
...projection.toIdentifierType(operation, context)
}),
exportPath: projection.toExportPath({ operation, enrichments, variant }),
enrichments,
variant
})
const instance = new projection({
context,
operation,
settings // ← passed in
})Inside the Projection, the settings are available as this.settings:
// ShadcnFormBase = toOasOperationProjectionBase({...}) in base.ts
class ShadcnForm extends ShadcnFormBase {
override toString(): string {
const { title, submitLabel } = this.settings.enrichments.subject ?? {}
return `
<Form>
${title ? `<h2>${title}</h2>` : ''}
${this.fields}
<Button>${submitLabel ?? 'Submit'}</Button>
</Form>
`
}
}The identifier is used implicitly when the Projection's Definition
is rendered (export const ${this.settings.identifier.name} = ...).
In Drivers (computed from projection static methods)
Drivers (OasOperationDriver, OasModelDriver, GqlOperationDriver)
compute ContentSettings by calling the projection's static methods.
The flow:
// In OasOperationDriver (conceptually)
const enrichments = projection.toEnrichments({ operation, context, variant })
const name = projection.toIdentifierName({ operation, enrichments, variant })
const identifier = lang.toIdentifier({
name,
...projection.toIdentifierType(operation, context)
})
const exportPath = projection.toExportPath({ operation, enrichments, variant })
const settings = new ContentSettings({ identifier, exportPath, enrichments, variant })
const instance = new projection({ context, operation, settings })toIdentifierName and toExportPath are pure functions of
(operation, enrichments, variant). Two calls with the same inputs
produce the same name and path — which is why the cache key
(identifier.name, exportPath) is stable across generator
iterations. (toIdentifierType is context-aware and runs only on
cache-miss.)
In the cache key
GenerateContext.findDefinition({ name, exportPath }) searches by
those two fields exactly. They're derived from
ContentSettings.identifier.name and ContentSettings.exportPath,
respectively. The full ContentSettings instance isn't part of the
key — just the two scalars.
Why not the full object? Identity stability. The static methods recompute fresh objects on every call (they're pure), but their outputs are equal-by-content. Keying by scalars sidesteps the issue of comparing object references.
Examples
Empty (no enrichments)
const settings = ContentSettings.empty({
identifier: createVariable('userBody'),
exportPath: '@/types/userBody.generated.ts'
})
// In the Projection
// UserBodyBase = toModelProjectionBase({...}) in base.ts
class UserBody extends UserBodyBase {
override toString(): string {
// No enrichments available; type is undefined
return `z.object({ name: z.string() })`
}
}With enrichments
type EnrichmentSchema = {
title?: string
submitLabel?: string
}
const settings = new ContentSettings<EnrichmentSchema>({
identifier: createVariable('createUserForm'),
exportPath: '/forms/CreateUser.generated.tsx',
enrichments: { title: 'Create User', submitLabel: 'Create' }
})
// ShadcnFormBase = toOasOperationProjectionBase<EnrichmentSchema>({...}) in base.ts
class ShadcnForm extends ShadcnFormBase {
override toString(): string {
const { title, submitLabel } = this.settings.enrichments.subject ?? {}
return `<Form><h2>${title}</h2>...<Button>${submitLabel}</Button></Form>`
}
}Common questions
Why is enrichments inside settings rather than a separate constructor arg?
Settings is the bundle: identifier, exportPath, and enrichments all flow together from the Driver. Splitting them would require every Projection constructor to accept three args instead of one — and would make it harder for projection-base classes to forward them.
The single settings object is the consistent interface across
operation, model, and GQL projection bases.
Why does ContentSettings.empty exist if I can just pass enrichments: undefined?
Two reasons:
- Intent clarity —
ContentSettings.empty({...})reads as "no enrichments here," whilenew ContentSettings({..., enrichments: undefined})looks like an oversight. - Type correctness —
ContentSettings.emptyreturnsContentSettings<undefined>directly, so downstream code sees the precise type rather thanContentSettings<E>whereEcould be anything.
Functionally they're equivalent; semantically the factory is preferable.
Can I mutate settings after construction?
Don't. ContentSettings is treated as immutable by the engine.
Mutating this.settings.identifier.name after construction would
desync the cache key from the actual rendered name, leading to
duplicate or missing definitions.
If you need a different identifier for some reason, construct a
fresh ContentSettings and a fresh Definition. Don't reach into
the existing object.
What if my generator needs additional per-instance data beyond ContentSettings?
Pass it via the Projection's constructor args object:
// MyBase = toOasOperationProjectionBase({...}) in base.ts
class MyProjection extends MyBase {
constructor(args: {
context, operation, settings,
customData: { ... } // ← additional field
}) {
super(args)
this.customData = args.customData
}
}Then read it as this.customData in toString(). This is the
escape hatch for data that doesn't fit the identifier/path/enrichments
shape.
Where does ContentSettings not flow?
Snippets. Snippets don't have a settings object — they're anonymous
helpers, not addressable units. Snippets receive their parameters via
their constructor's args and have no identifier or exportPath of
their own.
If a Snippet needs enrichment data, the parent Projection extracts it
from this.settings.enrichments and passes it to the Snippet
constructor.
Related types
class ContentSettings<E = undefined> {
identifier: IdentifierBase
exportPath: string
enrichments: E
variant: string
}See also
- API: Identifier — what
settings.identifieris - API: Projection bases — how settings are constructed and forwarded
- API: GenerateContext —
findDefinition({ name, exportPath })uses the settings' values - Reference: enrichments shape — how
settings.enrichmentsis routed - Cross-generator coordination concept — why settings drive the cache key
- Projections and Snippets concept — settings' role in Projections
- Glossary: ContentSettings, Identifier, Enrichments