Emitter Development Guide
The emitter transforms TypeSpec definitions into runtime code for C#, Python, TypeScript, and Go. This guide covers how to modify code generation.
Architecture Overview
Section titled “Architecture Overview”Directoryagentschema-emitter/
Directorysrc/
- emitter.ts entry point - dispatches to generators
- ast.ts TypeSpec AST parsing (TypeNode, PropertyNode, BaseTestContext)
- test-context.ts shared test context builder for all languages
- decorators.ts custom decorator handling
- utilities.ts helper functions
- csharp.ts C# code generator
- python.ts Python code generator
- typescript.ts TypeScript code generator
- go.ts Go code generator
- markdown.ts documentation generator
Directorytemplates/ Nunjucks templates
Directorycsharp/
- …
Directorypython/
- …
Directorytypescript/
- …
Directorygo/
- …
Directorydist/ compiled output (git-ignored)
- …
flowchart LR
A[TypeSpec AST] --> B[emitter.ts]
B --> C[ast.ts<br/>resolveModel]
C --> D[TypeNode Tree]
D --> E[csharp.ts]
D --> F[python.ts]
D --> G[typescript.ts]
D --> N[go.ts]
E --> H[templates/csharp/*.njk]
F --> I[templates/python/*.njk]
G --> J[templates/typescript/*.njk]
N --> O[templates/go/*.njk]
H --> K[runtime/csharp/]
I --> L[runtime/python/]
J --> M[runtime/typescript/]
O --> P[runtime/go/]
subgraph "Test Generation"
TC[test-context.ts<br/>buildBaseTestContext] --> E
TC --> F
TC --> G
TC --> N
end
Key Concepts
Section titled “Key Concepts”TypeNode and PropertyNode
Section titled “TypeNode and PropertyNode”The ast.ts file defines the core data structures passed to templates:
class TypeNode { typeName: { namespace: string; name: string }; description: string; base: TypeName | null; // Parent type childTypes: TypeNode[]; // Derived types properties: PropertyNode[]; isAbstract: boolean; discriminator?: string; alternates: Alternative[]; // Shorthand representations}
class PropertyNode { name: string; typeName: TypeName; description: string; isScalar: boolean; isOptional: boolean; isCollection: boolean; samples: SampleEntry[]; defaultValue: any;}Code Generation Flow
Section titled “Code Generation Flow”emitter.tsreceives the TypeSpec programresolveModel()builds the TypeNode tree from the root objectenumerateTypes()yields all types for code generation- Language-specific generators render templates for each type
Modifying Templates
Section titled “Modifying Templates”Templates use Nunjucks syntax. Each language has templates for:
| File | Purpose |
|---|---|
file.{ext}.njk | Main class definitions |
test.{ext}.njk | Test file generation (uses standardized context) |
_macros.njk | Shared template macros |
context.*.njk | LoadContext/SaveContext classes |
Test Context Standardization
Section titled “Test Context Standardization”All test templates use the shared buildBaseTestContext() function from test-context.ts. This ensures consistent field names across all languages:
// In your language generatorimport { buildBaseTestContext, yourLanguageTestOptions } from "./test-context.js";
function buildTestContext(node: TypeNode, packageName: string): BaseTestContext { return buildBaseTestContext(node, packageName, yourLanguageTestOptions);}Standardized field names in test templates:
| Field | Description |
|---|---|
validations | Array of property assertions (not validation) |
delimiter | Quote character for strings (not delimeter) |
scalarType | The scalar type name (not scalar) |
isOptional | Whether property is optional (not isPointer) |
package | Package/namespace (not packageName) |
Template Syntax
Section titled “Template Syntax”{# Comment #}{{ variable }} {# Output variable #}{{ value | lower }} {# Apply filter #}{% if condition %}...{% endif %} {# Conditional #}{% for item in items %}...{% endfor %} {# Loop #}{%- ... -%} {# Trim whitespace #}Common Patterns
Section titled “Common Patterns”Rendering property names:
{# C# - PascalCase #}{{ renderName(prop.name) }}
{# Python - snake_case (handled in generator) #}{{ prop.name }}
{# TypeScript - camelCase #}{{ prop.name }}
{# Go - PascalCase (exported) #}{{ renderName(prop.name) }}Type mapping:
{% if prop.isScalar %} {{ typeMapper[prop.typeName.name] }}{% else %} {{ prop.typeName.name }}{% endif %}Handling optionals:
{# C# #}{{ type }}{% if prop.isOptional %}?{% endif %}
{# Python #}Optional[{{ type }}]
{# TypeScript #}{{ prop.name }}{% if prop.isOptional %}?{% endif %}: {{ type }}
{# Go - uses pointer for optional #}{% if prop.isOptional %}*{% endif %}{{ type }}Adding a New Feature
Section titled “Adding a New Feature”Example: Add a new property to generated classes
Section titled “Example: Add a new property to generated classes”- Edit the TypeScript generator (e.g.,
csharp.ts):
// In buildClassContext or similarconst context = { node, newFeature: computeNewFeature(node), // ...};- Update the template (e.g.,
templates/csharp/file.cs.njk):
{% if newFeature %}/// <summary>New feature documentation</summary>public string NewFeature => "{{ newFeature }}";{% endif %}- Rebuild and test:
cd agentschema-emitternpx tsccp -r src/templates dist/src/
cd ../agentschemanpm run generate
cd ../runtime/csharp && dotnet testYAML Generation
Section titled “YAML Generation”For test data, use the YAML library with proper escaping:
import * as YAML from "yaml";
const doc = new YAML.Document(sample);YAML.visit(doc, { Scalar(key, node) { if (typeof node.value === 'string') { const str = node.value as string; // Quote strings with special characters if (str.includes('\n') || str.includes('#')) { node.type = 'QUOTE_DOUBLE'; } } }});const yaml = doc.toString({ indent: 2, lineWidth: 0 });Build Process
Section titled “Build Process”cd agentschema-emitter
# Compile TypeScriptnpx tsc
# Copy templates (REQUIRED - templates aren't compiled)cp -r src/templates dist/src/
# On Windows PowerShell:Copy-Item -Recurse -Force src/templates dist/src/Testing Changes
Section titled “Testing Changes”After any emitter change, test all four runtimes:
# C#cd runtime/csharp && dotnet test
# Pythoncd runtime/python/agentschema && uv run pytest tests/
# TypeScriptcd runtime/typescript/agentschema && npm test
# Gocd runtime/go/agentschema && go test ./...Debugging Tips
Section titled “Debugging Tips”- Print AST data: Add
console.log(JSON.stringify(node, null, 2))in generators - Check template context: Log the context object before rendering
- Inspect generated files: Look at output in
runtime/to verify changes - Run single runtime: Focus on one language while iterating
Common Issues
Section titled “Common Issues”Templates not updating
Section titled “Templates not updating”Forgot to copy templates to dist/src/. Run:
cp -r src/templates dist/src/Type errors in generated code
Section titled “Type errors in generated code”Check type mappers in the language generator (e.g., csharpTypeMapper).
Missing imports in generated files
Section titled “Missing imports in generated files”Update the imports array in the template context or _macros.njk.