skmtcdocs

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:

  1. In the rendered output — wrapped by Definition, this becomes the export const NAME = or export type Name = declaration.
  2. 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:

  1. Intent clarityContentSettings.empty({...}) reads as "no enrichments here," while new ContentSettings({..., enrichments: undefined}) looks like an oversight.
  2. Type correctnessContentSettings.empty returns ContentSettings<undefined> directly, so downstream code sees the precise type rather than ContentSettings<E> where E could 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.

class ContentSettings<E = undefined> {
  identifier: IdentifierBase
  exportPath: string
  enrichments: E
  variant: string
}

See also

On this page