When you write a custom Mustache template that drives a templateGenerator()
instance, the data dict your template receives follows a stable, typed shape
exported from @metaobjectsdev/codegen-ts/generators. This page documents
those shapes — they are public API. Field keys are versioned per
MetaObjects major release; deprecations are announced before removal.
templateGenerator() separates three concerns:
| Layer | Owner | Contract |
|---|---|---|
| Data extraction | per-port walker (TS / C# / Python / Java / Kotlin) | typed data dict |
| Template rendering | shared Mustache via @metaobjectsdev/render |
Mustache spec |
| File emission | the runner + overwrite policy | three-way merge |
The middle layer is language-agnostic: the same Mustache template that
renders <Entity>.md from TypeScript walks identically from C# or Python.
That only holds if the data dict shape is stable across ports and across
upgrades — hence this contract.
The per-entity Markdown documentation page. Populated by
buildEntityDocData(entity, opts).
interface EntityDocData {
generatedMarker: string; // "<!-- @generated by ... -->"
entity: {
name: string; // "Author"
type: string; // "object.entity"
source?: string; // "meta.blog.json"
package?: string; // "acme::blog"
description?: string;
};
descriptionQuote?: string; // "> ..." (each line of description)
preambleHeader: string; // "**Type:** ...\n**Source:** ...\n**Package:** ..."
storage?: { tableHeader: string; rows: StorageFieldDoc[] };
hasStorage?: boolean;
identities?: IdentityDoc[];
hasIdentities?: boolean;
relationships?: RelationshipDoc[];
hasRelationships?: boolean;
validation: {
insertSchema: string; // "AuthorInsertSchema"
updateSchema: string; // "AuthorUpdateSchema"
entityFile: string; // "Author.ts"
lower: string; // "author"
};
usedBy?: UsedByDoc[];
hasUsedBy?: boolean;
generated: GeneratedFileDoc[]; // "Generated code" bullets
}Each StorageFieldDoc / IdentityDoc / RelationshipDoc / UsedByDoc
carries one or more already-rendered strings (e.g. rowLine, bullet)
the template emits via {{{rowLine}}}. The escaping rules for Markdown
table pipes, code-spans, etc. live in the builder so templates stay
trivial and cross-port walkers don't have to re-derive the rules.
Adopter override path. Drop a templates/docs/entity-page.md.mustache
file into your project; the project's templates/ directory takes precedence
over the framework defaults. Same for any partial under templates/docs/.
The declarative template scopes (scope: "perEntity" | "perPackage" | "perModel"
on templateGenerator) feed a neutral, structural data dict — deliberately
distinct from the Markdown-flavored EntityDocData above. It carries raw
structural facts only, so a consumer's template can emit any language's code from
it. The field names are a byte-gated cross-port contract (the
fixtures/template-codegen-conformance/ corpus locks them) — change them only via
the spec.
interface FieldTemplateData {
name: string;
type: string; // neutral subtype: "string" | "int" | "currency" | "enum" | ...
required: boolean;
isArray: boolean; // arrayness is carried here, NOT appended to `type`
maxLength?: number;
enumValues?: string[]; // present only for field.enum
}
interface IdentityTemplateData { kind: string; fields: string[]; }
interface RelationshipTemplateData { name: string; cardinality: string; targetRef: string; }
interface EntityTemplateData {
name: string;
package: string; // effective package (`::`-separated), "" when none
fields: FieldTemplateData[];
identities: IdentityTemplateData[];
relationships: RelationshipTemplateData[];
}
interface PackageTemplateData { package: string; entities: EntityTemplateData[]; }
interface ModelTemplateData { packages: PackageTemplateData[]; } // packages ascendingThese types and their builders (buildEntityTemplateData,
buildPackageTemplateData, buildModelTemplateData) are exported from the package
main entry (@metaobjectsdev/codegen-ts), alongside expandOutputPattern and
the JSON template-spec parser (parseTemplateSpec, templateSpecToGenerators).
Abstract objects are excluded from every scope (they emit no instance artifact).
The Python port mirrors this contract dict (plain dict/list) under
metaobjects.codegen.template_codegen: build_entity_template_data /
build_package_template_data / build_model_template_data (in template_data),
expand_output_pattern (in output_pattern), and the JSON template-spec parser
parse_template_spec / template_spec_to_generators (in template_spec). Optional
keys (maxLength, enumValues) are omitted when absent so a {{#maxLength}}
section gates identically to TS.
The C# port mirrors the same contract as a Dictionary<string, object?> under
MetaObjects.Codegen.TemplateCodegen: TemplateData.Entity / TemplateData.Package
/ TemplateData.Model (Stubble renders the dictionaries), OutputPattern.Expand, and
the JSON template-spec parser TemplateSpec.Parse / TemplateSpec.ToGenerators.
Optional keys are likewise omitted when absent. Own-vs-effective discipline matches
the other ports (own-only for @required / maxLength / identity fields / relationship
cardinality+objectRef; effective for the required-validator and enum values).
Mustache cleanly iterates an array ({{#identities}}...{{/identities}}) but
has no native "is this array non-empty?" check that doesn't also iterate.
To emit a section header (## Identity) ONCE per section, we provide both:
identities: IdentityDoc[]— drive the bullet loophasIdentities: true— drive the section-header conditional
{{#hasIdentities}}
## Identity
{{#identities}}
- {{{bullet}}}
{{/identities}}
{{/hasIdentities}}Same pattern for hasStorage, hasRelationships, hasUsedBy.
- Adding a new field is a minor-version event (
0.7.x → 0.8.x). Existing templates keep working. - Renaming or removing a field requires a deprecation cycle:
- Major version N: the new name is added; the old name is kept and
emits a deprecation warning at codegen time. The
meta verify --strictcheck fails on use of the deprecated key. - Major version N+1: the old name is removed.
- Major version N: the new name is added; the old name is kept and
emits a deprecation warning at codegen time. The
- Re-shaping a nested object (e.g. flattening
validation.lowerto top-levellowerName) is a breaking change requiring the same deprecation cycle.
The conformance fixtures gate the shapes per port: any port whose walker emits an out-of-spec shape fails the cross-port conformance suite.
import type {
EntityDocData,
StorageFieldDoc,
IdentityDoc,
RelationshipDoc,
UsedByDoc,
GeneratedFileDoc,
} from "@metaobjectsdev/codegen-ts/generators";
import { buildEntityDocData, templateGenerator } from "@metaobjectsdev/codegen-ts/generators";Each type is intentionally narrow — fields are required iff the corresponding
section MUST appear in every entity's docs output. Optional fields use
? and templates check via {{#fieldName}}...{{/fieldName}}.
To add a new shape (e.g. for an HTML docs site, an OpenAPI spec, or a Mermaid diagram):
- Export a typed
interface XxxDatafrom a sibling module ofdocs-data.ts. - Provide a
buildXxxData(input, opts)builder. - Ship a default template at
templates/<group>/<source>.mustacheinsidecodegen-ts. - Add a conformance fixture that locks the expected output.
- Document the new shape on this page under a new section.
The templateGenerator({ walk, template, format }) factory does the rest —
no new generator scaffolding is needed.