@typra/emitter 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,5 +2,6 @@ import { EmitContext } from "@typespec/compiler";
2
2
  import { TypraEmitterOptions } from "../lib.js";
3
3
  export declare function emitGeneratedFile(context: EmitContext<TypraEmitterOptions>, filePath: string, content: string, options?: {
4
4
  marker?: boolean;
5
+ outputRoot?: string;
5
6
  }): Promise<void>;
6
7
  export declare function emitGeneratedManifest(context: EmitContext<TypraEmitterOptions>): Promise<void>;
@@ -4,7 +4,7 @@ const generatedFilesByProgram = new WeakMap();
4
4
  export async function emitGeneratedFile(context, filePath, content, options = {}) {
5
5
  const marker = options.marker ?? shouldMark(filePath);
6
6
  const finalContent = marker ? addMarker(filePath, content) : content;
7
- recordGeneratedFile(context.program, filePath, marker);
7
+ recordGeneratedFile(context.program, filePath, marker, options.outputRoot);
8
8
  await emitFile(context.program, {
9
9
  path: filePath,
10
10
  content: finalContent,
@@ -24,13 +24,14 @@ export async function emitGeneratedManifest(context) {
24
24
  content: JSON.stringify(manifest, null, 2),
25
25
  });
26
26
  }
27
- function recordGeneratedFile(program, filePath, marker) {
27
+ function recordGeneratedFile(program, filePath, marker, outputRoot) {
28
28
  let entries = generatedFilesByProgram.get(program);
29
29
  if (!entries) {
30
30
  entries = new Map();
31
31
  generatedFilesByProgram.set(program, entries);
32
32
  }
33
33
  entries.set(normalizePath(filePath), {
34
+ outputRoot: normalizePath(outputRoot || dirname(filePath)),
34
35
  path: normalizePath(filePath),
35
36
  marker,
36
37
  });
@@ -52,10 +53,6 @@ function markerFor(filePath) {
52
53
  return "// <auto-generated by typra-emitter>";
53
54
  }
54
55
  function normalizePath(filePath) {
55
- const absolute = resolve(filePath);
56
- const base = dirname(absolute);
57
- return relative(base, absolute).startsWith("..")
58
- ? absolute.replace(/\\/g, "/")
59
- : filePath.replace(/\\/g, "/");
56
+ return relative(process.cwd(), resolve(filePath)).replace(/\\/g, "/");
60
57
  }
61
58
  //# sourceMappingURL=generated-file.js.map
@@ -0,0 +1,50 @@
1
+ import { EmitContext } from "@typespec/compiler";
2
+ import { EmitTarget, TypraEmitterOptions } from "./lib.js";
3
+ import { TypeNode } from "./ir/ast.js";
4
+ export interface ExportSurfaceMethod {
5
+ name: string;
6
+ returns: string;
7
+ params: Record<string, string>;
8
+ optional: boolean;
9
+ sync: boolean;
10
+ }
11
+ export interface ExportSurfaceProtocol {
12
+ name: string;
13
+ group: string;
14
+ methods: ExportSurfaceMethod[];
15
+ }
16
+ export interface ExportSurfaceEntry {
17
+ name: string;
18
+ kind: "type" | "value";
19
+ group: string;
20
+ source: string;
21
+ protocol: boolean;
22
+ }
23
+ export interface ExportSurfaceGroup {
24
+ name: string;
25
+ exports: string[];
26
+ modules: string[];
27
+ }
28
+ export interface TargetExportSurface {
29
+ target: string;
30
+ outputRoot: string;
31
+ packageName?: string;
32
+ namespace?: string;
33
+ rootExports: string[];
34
+ exports: ExportSurfaceEntry[];
35
+ groups: ExportSurfaceGroup[];
36
+ protocols: ExportSurfaceProtocol[];
37
+ modules: string[];
38
+ }
39
+ export interface ExportSurfaceSnapshot {
40
+ emitter: "typra-emitter";
41
+ version: 1;
42
+ root: {
43
+ object: string;
44
+ namespace: string;
45
+ alias: string;
46
+ };
47
+ targets: TargetExportSurface[];
48
+ }
49
+ export declare function buildExportSurfaceSnapshot(rootObject: string, rootNamespace: string, rootAlias: string, targets: EmitTarget[], nodes: TypeNode[]): ExportSurfaceSnapshot;
50
+ export declare function emitExportSurfaceSnapshot(context: EmitContext<TypraEmitterOptions>, snapshot: ExportSurfaceSnapshot): Promise<void>;
@@ -0,0 +1,176 @@
1
+ import { emitFile, resolvePath } from "@typespec/compiler";
2
+ import { toKebabCase, toSnakeCase } from "./ir/utilities.js";
3
+ export function buildExportSurfaceSnapshot(rootObject, rootNamespace, rootAlias, targets, nodes) {
4
+ return {
5
+ emitter: "typra-emitter",
6
+ version: 1,
7
+ root: {
8
+ object: rootObject,
9
+ namespace: rootNamespace,
10
+ alias: rootAlias,
11
+ },
12
+ targets: targets
13
+ .map((target) => buildTargetSurface(rootNamespace, target, nodes))
14
+ .sort((left, right) => left.target.localeCompare(right.target)),
15
+ };
16
+ }
17
+ export async function emitExportSurfaceSnapshot(context, snapshot) {
18
+ await emitFile(context.program, {
19
+ path: resolvePath(context.emitterOutputDir, ".typra-generated", "export-surfaces.json"),
20
+ content: `${JSON.stringify(snapshot, null, 2)}\n`,
21
+ });
22
+ }
23
+ function buildTargetSurface(rootNamespace, target, nodes) {
24
+ const targetName = normalizeTarget(target.type);
25
+ const baseTypes = nodes.filter((node) => !node.base).sort(compareNodes);
26
+ const groups = buildGroups(targetName, baseTypes);
27
+ const exports = buildExports(targetName, baseTypes);
28
+ const rootExports = uniqueSorted(exports.map((entry) => entry.name));
29
+ const protocols = nodes
30
+ .filter((node) => node.isProtocol)
31
+ .sort(compareNodes)
32
+ .map((node) => ({
33
+ name: node.typeName.name,
34
+ group: node.group || "",
35
+ methods: node.methods
36
+ .map((method) => ({
37
+ name: method.name,
38
+ returns: method.returns,
39
+ params: sortRecord(method.params),
40
+ optional: method.optional,
41
+ sync: method.sync,
42
+ }))
43
+ .sort((left, right) => left.name.localeCompare(right.name)),
44
+ }));
45
+ return {
46
+ target: targetName,
47
+ outputRoot: target["output-dir"] || targetName,
48
+ ...targetMetadata(rootNamespace, targetName, target),
49
+ rootExports,
50
+ exports,
51
+ groups,
52
+ protocols,
53
+ modules: buildModules(targetName, baseTypes),
54
+ };
55
+ }
56
+ function buildExports(targetName, baseTypes) {
57
+ return baseTypes
58
+ .flatMap((node) => {
59
+ const group = node.group || "";
60
+ const source = sourceFor(targetName, node, group);
61
+ const kind = node.isProtocol ? "type" : "value";
62
+ return [node, ...node.childTypes].map((exportedNode) => ({
63
+ name: exportedNode.typeName.name,
64
+ kind,
65
+ group,
66
+ source,
67
+ protocol: node.isProtocol,
68
+ }));
69
+ })
70
+ .sort((left, right) => {
71
+ const byGroup = left.group.localeCompare(right.group);
72
+ if (byGroup !== 0)
73
+ return byGroup;
74
+ return left.name.localeCompare(right.name);
75
+ });
76
+ }
77
+ function buildGroups(targetName, baseTypes) {
78
+ const groupMap = new Map();
79
+ for (const node of baseTypes) {
80
+ const group = node.group || "";
81
+ if (!group)
82
+ continue;
83
+ if (!groupMap.has(group))
84
+ groupMap.set(group, []);
85
+ groupMap.get(group).push(node);
86
+ }
87
+ return Array.from(groupMap.entries())
88
+ .map(([name, groupNodes]) => ({
89
+ name,
90
+ exports: uniqueSorted(groupNodes.flatMap((node) => [node.typeName.name, ...node.childTypes.map((child) => child.typeName.name)])),
91
+ modules: uniqueSorted(groupNodes.map((node) => groupModuleName(targetName, node))),
92
+ }))
93
+ .sort((left, right) => left.name.localeCompare(right.name));
94
+ }
95
+ function buildModules(targetName, baseTypes) {
96
+ if (targetName === "rust") {
97
+ return uniqueSorted(["context", ...baseTypes.map((node) => node.group || moduleName(node))]);
98
+ }
99
+ return uniqueSorted(baseTypes.map((node) => sourceFor(targetName, node, node.group || "")));
100
+ }
101
+ function targetMetadata(rootNamespace, targetName, target) {
102
+ if (targetName === "go") {
103
+ return {
104
+ packageName: target["package-name"] || goPackageNameFromNamespace(rootNamespace),
105
+ };
106
+ }
107
+ if (targetName === "csharp") {
108
+ return {
109
+ namespace: target.namespace || rootNamespace,
110
+ };
111
+ }
112
+ if (targetName === "typescript") {
113
+ return {
114
+ namespace: target.namespace || rootNamespace.replace(/\.Core$/, ""),
115
+ };
116
+ }
117
+ if (targetName === "python") {
118
+ return {
119
+ packageName: rootNamespace.toLowerCase(),
120
+ };
121
+ }
122
+ return {};
123
+ }
124
+ function sourceFor(targetName, node, group) {
125
+ const name = node.typeName.name;
126
+ switch (targetName) {
127
+ case "typescript":
128
+ return group ? `./${group}/${toKebabCase(name)}` : `./${toKebabCase(name)}`;
129
+ case "python":
130
+ return group ? `.${group}` : `._${name}`;
131
+ case "rust":
132
+ return group ? `${group}::${toSnakeCase(name)}` : toSnakeCase(name);
133
+ case "go":
134
+ return `${toSnakeCase(name)}.go`;
135
+ case "csharp":
136
+ return group ? `${group}/${name}.cs` : `${name}.cs`;
137
+ default:
138
+ return name;
139
+ }
140
+ }
141
+ function moduleName(node) {
142
+ return toSnakeCase(node.typeName.name);
143
+ }
144
+ function groupModuleName(targetName, node) {
145
+ switch (targetName) {
146
+ case "typescript":
147
+ return toKebabCase(node.typeName.name);
148
+ case "python":
149
+ return `_${node.typeName.name}`;
150
+ case "csharp":
151
+ return `${node.typeName.name}.cs`;
152
+ default:
153
+ return moduleName(node);
154
+ }
155
+ }
156
+ function normalizeTarget(target) {
157
+ return target.toLowerCase().trim();
158
+ }
159
+ function goPackageNameFromNamespace(namespace) {
160
+ return namespace.toLowerCase().replace(/\./g, "");
161
+ }
162
+ function compareNodes(left, right) {
163
+ const leftGroup = left.group || "";
164
+ const rightGroup = right.group || "";
165
+ const byGroup = leftGroup.localeCompare(rightGroup);
166
+ if (byGroup !== 0)
167
+ return byGroup;
168
+ return left.typeName.name.localeCompare(right.typeName.name);
169
+ }
170
+ function uniqueSorted(values) {
171
+ return Array.from(new Set(values)).sort();
172
+ }
173
+ function sortRecord(record) {
174
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
175
+ }
176
+ //# sourceMappingURL=contract-surface.js.map
@@ -10,4 +10,5 @@ export interface GeneratorOptions {
10
10
  * Matches against model name (e.g., "AgentManifest") or fully qualified name (e.g., "Prompty.AgentManifest")
11
11
  */
12
12
  export declare function filterNodes(nodes: TypeNode[], options?: GeneratorOptions): TypeNode[];
13
+ export declare function inferRootNamespace(rootObject: string): string;
13
14
  export declare function $onEmit(context: EmitContext<TypraEmitterOptions>): Promise<void>;
@@ -7,6 +7,7 @@ import { generateTypeScript } from "./languages/typescript/driver.js";
7
7
  import { generateGo } from "./languages/go/driver.js";
8
8
  import { generateRust } from "./languages/rust/driver.js";
9
9
  import { emitGeneratedFile, emitGeneratedManifest } from "./cleanup/generated-file.js";
10
+ import { buildExportSurfaceSnapshot, emitExportSurfaceSnapshot } from "./contract-surface.js";
10
11
  /**
11
12
  * Filter nodes based on omit-models option.
12
13
  * Matches against model name (e.g., "AgentManifest") or fully qualified name (e.g., "Prompty.AgentManifest")
@@ -36,6 +37,28 @@ export function filterNodes(nodes, options) {
36
37
  return !omitModels.includes(name) && !omitModels.includes(fullName);
37
38
  });
38
39
  }
40
+ export function inferRootNamespace(rootObject) {
41
+ const lastDot = rootObject.lastIndexOf(".");
42
+ return lastDot > 0 ? rootObject.slice(0, lastDot) : "Typra";
43
+ }
44
+ function inferRootAlias(rootNamespace) {
45
+ return rootNamespace.split(".").filter(Boolean).at(-1) || rootNamespace || "Typra";
46
+ }
47
+ function isUninstantiatedTemplate(model) {
48
+ return !!(model.node &&
49
+ "templateParameters" in model.node &&
50
+ model.node.templateParameters.length > 0 &&
51
+ !model.templateMapper);
52
+ }
53
+ function collectNamespaceModels(namespace, models = []) {
54
+ for (const [, model] of namespace.models) {
55
+ models.push(model);
56
+ }
57
+ for (const [, childNamespace] of namespace.namespaces) {
58
+ collectNamespaceModels(childNamespace, models);
59
+ }
60
+ return models;
61
+ }
39
62
  // Registry of available code generators
40
63
  const generators = {
41
64
  markdown: generateMarkdown,
@@ -57,7 +80,9 @@ export async function $onEmit(context) {
57
80
  if (!m[0] || m[0].kind !== "Model") {
58
81
  throw new Error(`${rootObject} model not found or is not a model type.`);
59
82
  }
60
- const model = resolveModel(context.program, m[0], new Set(), options["root-namespace"] || "Typra", options["root-alias"] || "Typra");
83
+ const rootNamespace = options["root-namespace"] || inferRootNamespace(rootObject);
84
+ const rootAlias = options["root-alias"] || inferRootAlias(rootNamespace);
85
+ const model = resolveModel(context.program, m[0], new Set(), rootNamespace, rootAlias);
61
86
  if (options["root-alias"]) {
62
87
  model.typeName = {
63
88
  namespace: model.typeName.namespace,
@@ -67,7 +92,6 @@ export async function $onEmit(context) {
67
92
  // Discover additional models not reachable from the root.
68
93
  // If root-namespace is specified, resolve all models in that namespace
69
94
  // so new types are automatically emitted without manual additional-roots.
70
- const rootNamespace = options["root-namespace"] || "Typra";
71
95
  const additionalModels = [];
72
96
  const visited = new Set();
73
97
  // Collect names already in the main model tree to avoid duplicates
@@ -90,16 +114,15 @@ export async function $onEmit(context) {
90
114
  const nsRef = context.program.resolveTypeReference(rootNamespace);
91
115
  if (nsRef[0] && nsRef[0].kind === "Namespace") {
92
116
  const ns = nsRef[0];
93
- for (const [, nsModel] of ns.models) {
117
+ for (const nsModel of collectNamespaceModels(ns)) {
94
118
  const fullName = `${rootNamespace}.${nsModel.name}`;
95
119
  if (visited.has(fullName))
96
120
  continue;
97
121
  // Skip uninstantiated template declarations (e.g., Named<T>, Id<T>)
98
- if (nsModel.node && 'templateParameters' in nsModel.node &&
99
- nsModel.node.templateParameters.length > 0 && !nsModel.templateMapper) {
122
+ if (isUninstantiatedTemplate(nsModel)) {
100
123
  continue;
101
124
  }
102
- const additionalNode = resolveModel(context.program, nsModel, new Set(), rootNamespace, options["root-alias"] || "Typra");
125
+ const additionalNode = resolveModel(context.program, nsModel, new Set(), rootNamespace, rootAlias);
103
126
  additionalModels.push(additionalNode);
104
127
  visited.add(fullName);
105
128
  }
@@ -114,7 +137,7 @@ export async function $onEmit(context) {
114
137
  console.warn(`Warning: additional-root '${rootName}' not found or is not a model type. Skipping.`);
115
138
  continue;
116
139
  }
117
- const additionalNode = resolveModel(context.program, ref[0], new Set(), rootNamespace, options["root-alias"] || "Typra");
140
+ const additionalNode = resolveModel(context.program, ref[0], new Set(), rootNamespace, rootAlias);
118
141
  additionalModels.push(additionalNode);
119
142
  visited.add(rootName);
120
143
  }
@@ -123,6 +146,10 @@ export async function $onEmit(context) {
123
146
  omitModels: options["omit-models"] || [],
124
147
  additionalModels: additionalModels,
125
148
  };
149
+ const exportSurfaceNodes = filterNodes(Array.from(enumerateTypes(model)), {
150
+ omitModels: generatorOptions.omitModels,
151
+ additionalModels: [...additionalModels],
152
+ });
126
153
  // Dispatch to registered generators
127
154
  for (const target of targets) {
128
155
  const generatorName = target.type.toLowerCase().trim();
@@ -132,6 +159,7 @@ export async function $onEmit(context) {
132
159
  }
133
160
  }
134
161
  await emitGeneratedFile(context, resolvePath(context.emitterOutputDir, "json-ast", "model.json"), JSON.stringify(model.getSanitizedObject(), null, 2), { marker: false });
162
+ await emitExportSurfaceSnapshot(context, buildExportSurfaceSnapshot(rootObject, rootNamespace, rootAlias, targets, exportSurfaceNodes));
135
163
  await emitGeneratedManifest(context);
136
164
  }
137
165
  //# sourceMappingURL=emitter.js.map
@@ -34,7 +34,6 @@ const getModelType = (model, rootNamespace, rootAlias) => {
34
34
  * We use `any` because TypeSpec does not export `Node`, `SyntaxKind`,
35
35
  * or `TypeSpecScriptNode` from its public API surface.
36
36
  */
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
37
  function getNodeFilePath(node) {
39
38
  let current = node;
40
39
  while (current) {
@@ -461,7 +461,21 @@ function resolveImports(rootNode, types, registry) {
461
461
  * Handles formats like "Prompty", "Message[]", "Record<unknown>", "string", "unknown".
462
462
  */
463
463
  function extractMethodTypeRefs(method) {
464
- const SCALARS = new Set(["string", "int32", "float32", "float64", "boolean", "unknown"]);
464
+ const SCALARS = new Set([
465
+ "void",
466
+ "string",
467
+ "number",
468
+ "integer",
469
+ "int32",
470
+ "int64",
471
+ "float",
472
+ "float32",
473
+ "float64",
474
+ "numeric",
475
+ "boolean",
476
+ "unknown",
477
+ "any",
478
+ ]);
465
479
  const refs = [];
466
480
  const extract = (typeStr) => {
467
481
  // Strip nullable suffix and array suffix: "string?" → "string", "Message[]" → "Message"
@@ -72,7 +72,7 @@ export const generateCsharp = async (context, node, emitTarget, options) => {
72
72
  const csEnumName = field.enumName.charAt(0).toUpperCase() + field.enumName.slice(1);
73
73
  const grp = enumGroup.get(field.enumName) || "";
74
74
  const enumOutDir = grp ? `${emitTarget["output-dir"]}/${grp}` : emitTarget["output-dir"];
75
- await emitCsharpFile(context, nodes[0], enumCode, `${csEnumName}.cs`, enumOutDir);
75
+ await emitCsharpFile(context, nodes[0], enumCode, `${csEnumName}.cs`, enumOutDir, emitTarget["output-dir"]);
76
76
  }
77
77
  }
78
78
  }
@@ -81,10 +81,10 @@ export const generateCsharp = async (context, node, emitTarget, options) => {
81
81
  const classCode = emitCSharpClass(typeDecl, csharpNamespace, visitor, allTypeDecls, findTypeDecl);
82
82
  // Emit into group subfolder (C# uses namespaces, no re-export files needed)
83
83
  const outDir = n.group ? `${emitTarget["output-dir"]}/${n.group}` : emitTarget["output-dir"];
84
- await emitCsharpFile(context, n, classCode, `${n.typeName.name}.cs`, outDir);
84
+ await emitCsharpFile(context, n, classCode, `${n.typeName.name}.cs`, outDir, emitTarget["output-dir"]);
85
85
  if (emitTarget["test-dir"] && !n.isProtocol) {
86
86
  const testDir = n.group ? `${emitTarget["test-dir"]}/${n.group}` : emitTarget["test-dir"];
87
- await emitCsharpFile(context, n, renderTests(n, csharpNamespace), `${n.typeName.name}ConversionTests.cs`, testDir);
87
+ await emitCsharpFile(context, n, renderTests(n, csharpNamespace), `${n.typeName.name}ConversionTests.cs`, testDir, emitTarget["test-dir"]);
88
88
  }
89
89
  }
90
90
  // Format emitted files if format option is enabled (default: true)
@@ -250,13 +250,13 @@ const renderCsharpFactoryTestValue = (typeStr) => {
250
250
  default: return '"test"';
251
251
  }
252
252
  };
253
- const emitCsharpFile = async (context, type, python, filename, outputDir) => {
253
+ const emitCsharpFile = async (context, type, python, filename, outputDir, outputRoot) => {
254
254
  outputDir = outputDir || `${context.emitterOutputDir}/CSharp`;
255
255
  const typePath = type.typeName.namespace.split(".");
256
256
  // replace typename with file
257
257
  typePath.push(filename);
258
258
  const path = resolvePath(outputDir, filename);
259
- await emitGeneratedFile(context, path, python);
259
+ await emitGeneratedFile(context, path, python, { outputRoot: outputRoot || outputDir });
260
260
  };
261
261
  /**
262
262
  * Format C# files using dotnet format.
@@ -136,6 +136,8 @@ function protocolCSharpType(typeStr) {
136
136
  return "Dictionary<string, object?>";
137
137
  if (typeStr === "unknown" || typeStr === "any")
138
138
  return "object";
139
+ if (typeStr === "void")
140
+ return "void";
139
141
  // Handle nullable types (e.g., "string?")
140
142
  if (typeStr.endsWith("?")) {
141
143
  const inner = typeStr.slice(0, -1);
@@ -173,7 +175,9 @@ function emitCSharpInterface(type, namespace, lines) {
173
175
  // Synchronous method
174
176
  if (method.optional) {
175
177
  // Return type already includes nullability — provide default body
176
- lines.push(` ${ret} ${toPascalCase(method.name)}(${params}) => default!;`);
178
+ lines.push(ret === "void"
179
+ ? ` void ${toPascalCase(method.name)}(${params}) { }`
180
+ : ` ${ret} ${toPascalCase(method.name)}(${params}) => default!;`);
177
181
  }
178
182
  else {
179
183
  lines.push(` ${ret} ${toPascalCase(method.name)}(${params});`);
@@ -182,10 +186,17 @@ function emitCSharpInterface(type, namespace, lines) {
182
186
  else {
183
187
  // Async method
184
188
  if (method.optional) {
185
- lines.push(` Task<${ret}> ${toPascalCase(method.name)}Async(${params}) => Task.FromResult<${ret}>(default!);`);
189
+ if (ret === "void") {
190
+ lines.push(` Task ${toPascalCase(method.name)}Async(${params}) => Task.CompletedTask;`);
191
+ }
192
+ else {
193
+ lines.push(` Task<${ret}> ${toPascalCase(method.name)}Async(${params}) => Task.FromResult<${ret}>(default!);`);
194
+ }
186
195
  }
187
196
  else {
188
- lines.push(` Task<${ret}> ${toPascalCase(method.name)}Async(${params});`);
197
+ lines.push(ret === "void"
198
+ ? ` Task ${toPascalCase(method.name)}Async(${params});`
199
+ : ` Task<${ret}> ${toPascalCase(method.name)}Async(${params});`);
189
200
  }
190
201
  }
191
202
  }
@@ -10,3 +10,4 @@ export declare const goTypeMapper: Record<string, string>;
10
10
  * Main entry point for Go code generation.
11
11
  */
12
12
  export declare const generateGo: (context: EmitContext<TypraEmitterOptions>, node: TypeNode, emitTarget: EmitTarget, options?: GeneratorOptions) => Promise<void>;
13
+ export declare function goPackageNameFromNamespace(namespace: string): string;
@@ -40,8 +40,7 @@ export const generateGo = async (context, node, emitTarget, options) => {
40
40
  // Build the expression IR infrastructure
41
41
  const registry = TypeRegistry.fromTypeGraph(allTypes);
42
42
  const visitor = new GoExprVisitor(registry);
43
- // Determine package name from root node namespace (e.g., "Typra" -> "typra")
44
- const packageName = node.typeName.namespace.toLowerCase().replace(/\./g, '');
43
+ const packageName = emitTarget["package-name"] || goPackageNameFromNamespace(node.typeName.namespace);
45
44
  // Collect all polymorphic type names across all nodes
46
45
  const polymorphicTypeNames = new Set();
47
46
  for (const n of nodes) {
@@ -61,7 +60,7 @@ export const generateGo = async (context, node, emitTarget, options) => {
61
60
  // Go stays flat: pass group as a header comment only, no subfolder emission
62
61
  const fileContent = emitGoFileContent(fileDecl.types, packageName, visitor, polymorphicTypeNames, fileDecl.enums, n.group || "");
63
62
  const fileName = toSnakeCase(n.typeName.name) + '.go';
64
- await emitGoFile(context, fileName, fileContent, emitTarget["output-dir"]);
63
+ await emitGoFile(context, fileName, fileContent, emitTarget["output-dir"], emitTarget["output-dir"]);
65
64
  }
66
65
  // Emit test file for each type (skip protocols — they have no data to test)
67
66
  if (emitTarget["test-dir"] && !n.isProtocol) {
@@ -69,7 +68,7 @@ export const generateGo = async (context, node, emitTarget, options) => {
69
68
  const testContext = { ...buildTestContext(n, packageName), importPath };
70
69
  const testContent = emitGoTest(testContext);
71
70
  const testFileName = toSnakeCase(n.typeName.name) + '_test.go';
72
- await emitGoFile(context, testFileName, testContent, emitTarget["test-dir"]);
71
+ await emitGoFile(context, testFileName, testContent, emitTarget["test-dir"], emitTarget["test-dir"]);
73
72
  }
74
73
  }
75
74
  // Format emitted files if format option is enabled (default: true)
@@ -117,12 +116,15 @@ function formatGoFiles(outputDir, testDir) {
117
116
  function buildTestContext(node, packageName) {
118
117
  return buildBaseTestContext(node, packageName, goTestOptions);
119
118
  }
119
+ export function goPackageNameFromNamespace(namespace) {
120
+ return namespace.toLowerCase().replace(/\./g, "");
121
+ }
120
122
  /**
121
123
  * Write generated Go content to file.
122
124
  */
123
- async function emitGoFile(context, filename, content, outputDir) {
125
+ async function emitGoFile(context, filename, content, outputDir, outputRoot) {
124
126
  outputDir = outputDir || `${context.emitterOutputDir}/go`;
125
127
  const filePath = resolvePath(outputDir, filename);
126
- await emitGeneratedFile(context, filePath, content);
128
+ await emitGeneratedFile(context, filePath, content, { outputRoot: outputRoot || outputDir });
127
129
  }
128
130
  //# sourceMappingURL=driver.js.map
@@ -814,12 +814,15 @@ function emitMethodStubs(typeName, methods, lines) {
814
814
  if (method.description) {
815
815
  lines.push(`\t// ${toPascalCase(method.name)} — ${method.description}`);
816
816
  }
817
- lines.push(`\t${toPascalCase(method.name)}() ${goMethodReturnType(method.returns)}`);
817
+ const ret = goMethodReturnType(method.returns);
818
+ lines.push(`\t${toPascalCase(method.name)}()${ret ? ` ${ret}` : ""}`);
818
819
  }
819
820
  lines.push("}");
820
821
  lines.push("");
821
822
  }
822
823
  function goMethodReturnType(returns) {
824
+ if (returns === "void")
825
+ return "";
823
826
  return GO_TYPE_MAP[returns] || returns;
824
827
  }
825
828
  // ============================================================================
@@ -841,8 +844,20 @@ function protocolGoType(typeStr) {
841
844
  return "map[string]interface{}";
842
845
  if (typeStr === "unknown" || typeStr === "any")
843
846
  return "interface{}";
847
+ if (typeStr === "void")
848
+ return "";
844
849
  return GO_TYPE_MAP[typeStr] || typeStr;
845
850
  }
851
+ function goProtocolReturn(method) {
852
+ const ret = protocolGoType(method.returns);
853
+ if (method.sync && method.optional) {
854
+ return ret ? ` ${ret}` : "";
855
+ }
856
+ if (method.sync) {
857
+ return ret ? ` (${ret}, error)` : " error";
858
+ }
859
+ return ret ? ` (${ret}, error)` : " error";
860
+ }
846
861
  /**
847
862
  * Emit a Go interface for a protocol type.
848
863
  */
@@ -859,19 +874,7 @@ function emitProtocolInterface(type, lines) {
859
874
  const params = Object.entries(method.params)
860
875
  .map(([pName, pType]) => `${pName} ${protocolGoType(pType)}`)
861
876
  .join(", ");
862
- const ret = protocolGoType(method.returns);
863
- if (method.sync) {
864
- // Sync method — return type without error for optional
865
- if (method.optional) {
866
- lines.push(`\t${toPascalCase(method.name)}(${params}) ${ret}`);
867
- }
868
- else {
869
- lines.push(`\t${toPascalCase(method.name)}(${params}) (${ret}, error)`);
870
- }
871
- }
872
- else {
873
- lines.push(`\t${toPascalCase(method.name)}(${params}) (${ret}, error)`);
874
- }
877
+ lines.push(`\t${toPascalCase(method.name)}(${params})${goProtocolReturn(method)}`);
875
878
  }
876
879
  lines.push("}");
877
880
  lines.push("");
@@ -403,6 +403,6 @@ export const generateCoercions = (node) => {
403
403
  };
404
404
  const emitMarkdownFile = async (context, name, markdown, outputDir) => {
405
405
  const dir = outputDir || `${context.emitterOutputDir}/markdown`;
406
- await emitGeneratedFile(context, resolvePath(dir, `${name}.md`), markdown);
406
+ await emitGeneratedFile(context, resolvePath(dir, `${name}.md`), markdown, { outputRoot: dir });
407
407
  };
408
408
  //# sourceMappingURL=driver.js.map
@@ -100,14 +100,14 @@ export const generatePython = async (context, node, emitTarget, options) => {
100
100
  const fileDecl = lowerFile(n, registry, polymorphicTypeNames);
101
101
  const fileContent = emitPythonFileDecl(fileDecl, visitor, group);
102
102
  const outDir = group ? `${emitTarget["output-dir"]}/${group}` : emitTarget["output-dir"];
103
- await emitPythonFile(context, `_${n.typeName.name}.py`, fileContent, outDir);
103
+ await emitPythonFile(context, `_${n.typeName.name}.py`, fileContent, outDir, emitTarget["output-dir"]);
104
104
  }
105
105
  // Render test file for each type (skip protocols — they have no data to test)
106
106
  if (emitTarget["test-dir"] && !n.isProtocol) {
107
107
  const testDir = n.group ? `${emitTarget["test-dir"]}/${n.group}` : emitTarget["test-dir"];
108
108
  const testContext = buildTestContext(n, importPath);
109
109
  const testContent = emitPythonTest(testContext);
110
- await emitPythonFile(context, `test_${toSnakeCase(n.typeName.name)}.py`, testContent, testDir);
110
+ await emitPythonFile(context, `test_${toSnakeCase(n.typeName.name)}.py`, testContent, testDir, emitTarget["test-dir"]);
111
111
  }
112
112
  }
113
113
  // Emit group-level __init__.py for each group
@@ -115,7 +115,7 @@ export const generatePython = async (context, node, emitTarget, options) => {
115
115
  if (!group)
116
116
  continue; // Root-level types (if any) are covered by the root __init__.py
117
117
  const groupInitContent = emitPythonGroupInit(group, groupNodes);
118
- await emitPythonFile(context, '__init__.py', groupInitContent, `${emitTarget["output-dir"]}/${group}`);
118
+ await emitPythonFile(context, '__init__.py', groupInitContent, `${emitTarget["output-dir"]}/${group}`, emitTarget["output-dir"]);
119
119
  }
120
120
  // Format emitted files if format option is enabled (default: true)
121
121
  if (emitTarget.format !== false) {
@@ -364,9 +364,9 @@ function getUniqueImportTypes(node) {
364
364
  /**
365
365
  * Write generated Python content to file using TypeSpec's emitFile API.
366
366
  */
367
- async function emitPythonFile(context, filename, content, outputDir) {
367
+ async function emitPythonFile(context, filename, content, outputDir, outputRoot) {
368
368
  outputDir = outputDir || `${context.emitterOutputDir}/python`;
369
369
  const filePath = resolvePath(outputDir, filename);
370
- await emitGeneratedFile(context, filePath, content);
370
+ await emitGeneratedFile(context, filePath, content, { outputRoot: outputRoot || outputDir });
371
371
  }
372
372
  //# sourceMappingURL=driver.js.map
@@ -309,6 +309,8 @@ function protocolType(typeStr) {
309
309
  return "dict[str, Any]";
310
310
  if (typeStr === "unknown" || typeStr === "any")
311
311
  return "Any";
312
+ if (typeStr === "void")
313
+ return "None";
312
314
  // Scalar types
313
315
  const mapped = TYPE_MAP[typeStr];
314
316
  if (mapped)
@@ -11,3 +11,14 @@ export declare const rustTypeMapper: Record<string, string>;
11
11
  * Main entry point for Rust code generation.
12
12
  */
13
13
  export declare const generateRust: (context: EmitContext<TypraEmitterOptions>, node: TypeNode, emitTarget: EmitTarget, options?: GeneratorOptions) => Promise<void>;
14
+ /**
15
+ * Emit the root mod.rs file content (module declarations).
16
+ *
17
+ * @param rootModules - Module names emitted directly in the root (e.g. ["context"])
18
+ * @param groups - Group subfolder names (e.g. ["connection", "tools"])
19
+ */
20
+ export declare function emitRustLib(rootModules: string[], groups?: string[]): string;
21
+ /**
22
+ * Emit a per-group mod.rs file that declares and re-exports all modules in that group.
23
+ */
24
+ export declare function emitRustGroupMod(moduleNames: string[]): string;
@@ -103,7 +103,7 @@ export const generateRust = async (context, node, emitTarget, options) => {
103
103
  const fileContent = emitRustFileDecl(fileDecl, visitor, polymorphicTypeNames, childToParent);
104
104
  const fileName = toSnakeCase(n.typeName.name) + '.rs';
105
105
  const outDir = group ? `${emitTarget["output-dir"]}/${group}` : emitTarget["output-dir"];
106
- await emitRustFile(context, fileName, fileContent, outDir);
106
+ await emitRustFile(context, fileName, fileContent, outDir, emitTarget["output-dir"]);
107
107
  if (!groupModuleNames.has(group))
108
108
  groupModuleNames.set(group, []);
109
109
  groupModuleNames.get(group).push(toSnakeCase(n.typeName.name));
@@ -121,7 +121,7 @@ export const generateRust = async (context, node, emitTarget, options) => {
121
121
  const testFileName = toSnakeCase(n.typeName.name) + '_test.rs';
122
122
  const testGroup = n.group || "";
123
123
  const testDir = testGroup ? `${emitTarget["test-dir"]}/${testGroup}` : emitTarget["test-dir"];
124
- await emitRustFile(context, testFileName, testContent, testDir);
124
+ await emitRustFile(context, testFileName, testContent, testDir, emitTarget["test-dir"]);
125
125
  if (!testGroupModuleNames.has(testGroup))
126
126
  testGroupModuleNames.set(testGroup, []);
127
127
  testGroupModuleNames.get(testGroup).push(toSnakeCase(n.typeName.name) + '_test');
@@ -132,7 +132,7 @@ export const generateRust = async (context, node, emitTarget, options) => {
132
132
  if (!group)
133
133
  continue; // Root-level types handled in root mod.rs
134
134
  const groupModContent = emitRustGroupMod(modules);
135
- await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["output-dir"]}/${group}`);
135
+ await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["output-dir"]}/${group}`, emitTarget["output-dir"]);
136
136
  }
137
137
  // Render test group mod.rs files and test main.rs
138
138
  if (emitTarget["test-dir"]) {
@@ -142,7 +142,7 @@ export const generateRust = async (context, node, emitTarget, options) => {
142
142
  if (group) {
143
143
  const groupModContent = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n\n'
144
144
  + testMods.map(m => `mod ${m};`).join('\n') + '\n';
145
- await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["test-dir"]}/${group}`);
145
+ await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["test-dir"]}/${group}`, emitTarget["test-dir"]);
146
146
  testGroups.push(group);
147
147
  }
148
148
  }
@@ -209,10 +209,10 @@ function buildTestContext(node) {
209
209
  /**
210
210
  * Write generated Rust content to file.
211
211
  */
212
- async function emitRustFile(context, filename, content, outputDir) {
212
+ async function emitRustFile(context, filename, content, outputDir, outputRoot) {
213
213
  outputDir = outputDir || `${context.emitterOutputDir}/rust`;
214
214
  const filePath = resolvePath(outputDir, filename);
215
- await emitGeneratedFile(context, filePath, `${content.trimEnd()}\n`);
215
+ await emitGeneratedFile(context, filePath, `${content.trimEnd()}\n`, { outputRoot: outputRoot || outputDir });
216
216
  }
217
217
  /**
218
218
  * Emit the context.rs file content (LoadContext/SaveContext structs).
@@ -398,7 +398,7 @@ impl SaveContext {
398
398
  * @param rootModules - Module names emitted directly in the root (e.g. ["context"])
399
399
  * @param groups - Group subfolder names (e.g. ["connection", "tools"])
400
400
  */
401
- function emitRustLib(rootModules, groups = []) {
401
+ export function emitRustLib(rootModules, groups = []) {
402
402
  let out = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n';
403
403
  for (const module of rootModules) {
404
404
  out += `\npub mod ${module};\npub use ${module}::*;\n`;
@@ -411,7 +411,7 @@ function emitRustLib(rootModules, groups = []) {
411
411
  /**
412
412
  * Emit a per-group mod.rs file that declares and re-exports all modules in that group.
413
413
  */
414
- function emitRustGroupMod(moduleNames) {
414
+ export function emitRustGroupMod(moduleNames) {
415
415
  let out = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n';
416
416
  for (const module of moduleNames) {
417
417
  out += `\npub mod ${module};\npub use ${module}::*;\n`;
@@ -990,14 +990,16 @@ function emitMethodTrait(type, lines) {
990
990
  if (method.description) {
991
991
  emitDocComment(method.description, " ", lines);
992
992
  }
993
- lines.push(` fn ${toSnakeCase(method.name)}(&self) -> ${methodReturnType(method)};`);
993
+ const params = Object.entries(method.params)
994
+ .map(([pName, pType]) => `${toSnakeCase(pName)}: &${protocolRustType(pType)}`)
995
+ .join(", ");
996
+ const signatureParams = params ? `, ${params}` : "";
997
+ lines.push(` fn ${toSnakeCase(method.name)}(&self${signatureParams}) -> ${methodReturnType(method)};`);
994
998
  }
995
999
  lines.push("}");
996
1000
  }
997
1001
  function methodReturnType(method) {
998
- if (method.returns === "string")
999
- return "String";
1000
- return RUST_TYPE_MAP[method.returns] || method.returns;
1002
+ return protocolRustType(method.returns);
1001
1003
  }
1002
1004
  // ============================================================================
1003
1005
  // Protocol trait emission
@@ -1018,6 +1020,8 @@ function protocolRustType(typeStr) {
1018
1020
  return "serde_json::Value";
1019
1021
  if (typeStr === "unknown" || typeStr === "any")
1020
1022
  return "serde_json::Value";
1023
+ if (typeStr === "void")
1024
+ return "()";
1021
1025
  if (typeStr === "string")
1022
1026
  return "String";
1023
1027
  return RUST_TYPE_MAP[typeStr] || typeStr;
@@ -1046,7 +1050,7 @@ function emitProtocolTrait(type, lines) {
1046
1050
  if (method.optional) {
1047
1051
  // Return type already includes nullability from ? suffix — don't double-wrap
1048
1052
  lines.push(` fn ${toSnakeCase(method.name)}(&self, ${params}) -> ${ret} {`);
1049
- lines.push(" None");
1053
+ lines.push(ret === "()" ? " ()" : " None");
1050
1054
  lines.push(" }");
1051
1055
  }
1052
1056
  else {
@@ -66,14 +66,14 @@ export const generateTypeScript = async (context, node, emitTarget, options) =>
66
66
  const fileDecl = lowerFile(n, registry, polymorphicTypeNames);
67
67
  const code = emitTypeScriptFileDecl(fileDecl, visitor, tsNamespace, group);
68
68
  const outDir = group ? `${emitTarget["output-dir"]}/${group}` : emitTarget["output-dir"];
69
- await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.ts`, code, outDir);
69
+ await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.ts`, code, outDir, emitTarget["output-dir"]);
70
70
  }
71
71
  // Emit group index.ts files
72
72
  for (const [group, groupNodes] of groupMap) {
73
73
  if (!group)
74
74
  continue;
75
75
  const groupIndexCode = emitTypeScriptGroupIndex(group, groupNodes);
76
- await emitTypeScriptFile(context, "index.ts", groupIndexCode, `${emitTarget["output-dir"]}/${group}`);
76
+ await emitTypeScriptFile(context, "index.ts", groupIndexCode, `${emitTarget["output-dir"]}/${group}`, emitTarget["output-dir"]);
77
77
  }
78
78
  // Emit test files for all types (skip protocols — they have no data to test)
79
79
  if (emitTarget["test-dir"]) {
@@ -91,7 +91,7 @@ export const generateTypeScript = async (context, node, emitTarget, options) =>
91
91
  importPath: testImportPath,
92
92
  namespace: tsNamespace,
93
93
  });
94
- await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.test.ts`, testCode, testDir);
94
+ await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.test.ts`, testCode, testDir, emitTarget["test-dir"]);
95
95
  }
96
96
  }
97
97
  // Emit root index.ts file — re-exports from group sub-indexes
@@ -201,9 +201,9 @@ function buildTestContext(node) {
201
201
  /**
202
202
  * Write generated TypeScript content to file.
203
203
  */
204
- async function emitTypeScriptFile(context, filename, content, outputDir) {
204
+ async function emitTypeScriptFile(context, filename, content, outputDir, outputRoot) {
205
205
  outputDir = outputDir || `${context.emitterOutputDir}/typescript`;
206
206
  const filePath = resolvePath(outputDir, filename);
207
- await emitGeneratedFile(context, filePath, content);
207
+ await emitGeneratedFile(context, filePath, content, { outputRoot: outputRoot || outputDir });
208
208
  }
209
209
  //# sourceMappingURL=driver.js.map
@@ -85,6 +85,8 @@ function returnType(typeStr) {
85
85
  }
86
86
  if (typeStr === "Record<unknown>")
87
87
  return "Record<string, unknown>";
88
+ if (typeStr === "void")
89
+ return "void";
88
90
  return TYPE_MAP[typeStr] || typeStr;
89
91
  }
90
92
  // ============================================================================
package/dist/src/lib.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface EmitTarget {
8
8
  "format"?: boolean;
9
9
  "namespace"?: string;
10
10
  "import-path"?: string;
11
+ "package-name"?: string;
11
12
  }
12
13
  export interface TypraEmitterOptions {
13
14
  "root-object": string;
package/dist/src/lib.js CHANGED
@@ -39,6 +39,11 @@ const TypraEmitterOptionsSchema = {
39
39
  type: "string",
40
40
  nullable: true,
41
41
  description: "Import path for generated code in tests. Defaults vary by language."
42
+ },
43
+ "package-name": {
44
+ type: "string",
45
+ nullable: true,
46
+ description: "Language package/module name override. Currently used by Go; defaults to the emitted root namespace."
42
47
  }
43
48
  },
44
49
  required: ["type"]
@@ -1,29 +1,48 @@
1
1
  import "@typra/emitter";
2
+ import "./model/events/session.tsp";
3
+ import "./model/pipeline/harness.tsp";
2
4
 
3
5
  namespace Typra.Fixtures;
4
6
 
5
7
  model FixtureRoot {
8
+ @sample(#{ name: "fixture-root" })
6
9
  @doc("Required scalar field")
7
10
  name: string;
8
11
 
12
+ @sample(#{ description: "A generated fixture with broad emitter coverage." })
9
13
  @doc("Optional scalar field")
10
14
  description?: string;
11
15
 
16
+ @sample(#{ tags: #["typespec", "emitter", "validation"] })
12
17
  @doc("Array of scalar tags")
13
18
  tags: string[];
14
19
 
20
+ @sample(#{ metadata: #{ source: "fixture", version: 1 } })
15
21
  @doc("Dictionary-shaped metadata")
16
22
  metadata?: Record<unknown>;
17
23
 
24
+ @sample(#{ owner: #{ id: "owner-1", displayName: "Fixture Owner" } })
18
25
  @doc("Nested object field")
19
26
  owner: FixtureOwner;
20
27
 
28
+ @sample(#{ content: #{ kind: "text", text: "hello from a polymorphic sample" } })
21
29
  @doc("Discriminated union field")
22
30
  content: FixtureContent;
31
+
32
+ @sample(#{ status: "ready" })
33
+ @doc("Closed string union field")
34
+ status: FixtureStatus;
35
+
36
+ @sample(#{ mode: "batch" })
37
+ @doc("Open string union field")
38
+ mode?: FixtureMode;
23
39
  }
24
40
 
25
41
  model FixtureOwner {
42
+ @sample(#{ id: "owner-1" })
26
43
  id: string;
44
+
45
+ @sample(#{ displayName: "Fixture Owner" })
27
46
  displayName?: string;
28
47
  }
29
48
 
@@ -34,10 +53,53 @@ model FixtureContent {
34
53
 
35
54
  model TextContent extends FixtureContent {
36
55
  kind: "text";
56
+
57
+ @sample(#{ text: "hello from text content" })
37
58
  text: string;
38
59
  }
39
60
 
40
61
  model ImageContent extends FixtureContent {
41
62
  kind: "image";
63
+
64
+ @sample(#{ url: "https://example.test/image.png" })
42
65
  url: string;
43
66
  }
67
+
68
+ alias FixtureStatus = "draft" | "ready" | "archived";
69
+ alias FixtureMode = "interactive" | "batch" | string;
70
+
71
+ @@coerce(FixtureReference, string, #{ id: "{value}", label: "coerced reference" }, "reference", "Load a reference from an id string.", "ref-coerced");
72
+ @@factory(FixtureReference, "named", #{ id: "{id}", label: "{label}" }, #{ id: "string", label: "string" });
73
+ @@method(FixtureReference, "display", "string", "Render the reference label.", #{ prefix: "string" }, true, true);
74
+ model FixtureReference {
75
+ @sample(#{ id: "ref-1" })
76
+ id: string;
77
+
78
+ @sample(#{ label: "Primary Reference" })
79
+ label?: string;
80
+ }
81
+
82
+ model FixtureTool {
83
+ @sample(#{ name: "search" })
84
+ name: string;
85
+
86
+ @sample(#{ command: "search --query" })
87
+ command: string;
88
+ }
89
+
90
+ model FixtureToolbox {
91
+ @sample(#{ tools: #[#{ name: "search", command: "search --query" }] })
92
+ tools: FixtureTool[];
93
+ }
94
+
95
+ @@knownAs(WireOptions.maxOutputTokens, "openai", "max_completion_tokens");
96
+ @@knownAs(WireOptions.maxOutputTokens, "anthropic", "max_tokens");
97
+ @@knownAs(WireOptions.temperature, "openai", "temperature");
98
+ @@defaultFor(WireOptions.temperature, "openai", 0.2);
99
+ model WireOptions {
100
+ @sample(#{ maxOutputTokens: 256 })
101
+ maxOutputTokens?: int32;
102
+
103
+ @sample(#{ temperature: 0.7 })
104
+ temperature?: float32;
105
+ }
@@ -0,0 +1,13 @@
1
+ import "@typra/emitter";
2
+
3
+ namespace Typra.Fixtures;
4
+
5
+ model Checkpoint {
6
+ id: string;
7
+ label?: string;
8
+ }
9
+
10
+ model SessionSummary {
11
+ sessionId: string;
12
+ latestCheckpoint?: Checkpoint;
13
+ }
@@ -0,0 +1,11 @@
1
+ import "@typra/emitter";
2
+
3
+ namespace Typra.Fixtures;
4
+
5
+ @@protocol(EventSink);
6
+ @@method(EventSink, "emit", "void", "Emit an event.", #{ event: "unknown" }, false, true);
7
+ model EventSink {}
8
+
9
+ @@protocol(CheckpointStore);
10
+ @@method(CheckpointStore, "save", "void", "Save a checkpoint.", #{ checkpoint: "Checkpoint" }, false, false);
11
+ model CheckpointStore {}
@@ -4,10 +4,33 @@ options:
4
4
  "@typra/emitter":
5
5
  emitter-output-dir: "{cwd}/generated/fixtures"
6
6
  root-object: "Typra.Fixtures.FixtureRoot"
7
- root-namespace: "Typra.Fixtures"
8
7
  emit-targets:
9
8
  - type: TypeScript
10
9
  output-dir: "generated/fixtures/typescript"
11
10
  test-dir: "generated/fixtures/typescript/tests"
12
11
  import-path: "../index"
13
12
  format: false
13
+ - type: Python
14
+ output-dir: "generated/fixtures/python"
15
+ test-dir: "generated/fixtures/python/tests"
16
+ import-path: "fixtures"
17
+ format: false
18
+ - type: Go
19
+ output-dir: "generated/fixtures/go"
20
+ test-dir: "generated/fixtures/go/tests"
21
+ import-path: "fixtures"
22
+ package-name: "fixtures"
23
+ format: false
24
+ - type: CSharp
25
+ output-dir: "generated/fixtures/csharp"
26
+ test-dir: "generated/fixtures/csharp/tests"
27
+ namespace: "Typra.Fixtures"
28
+ format: false
29
+ - type: Rust
30
+ output-dir: "generated/fixtures/rust"
31
+ test-dir: "generated/fixtures/rust/tests"
32
+ import-path: "fixtures::model"
33
+ format: false
34
+ - type: Markdown
35
+ output-dir: "generated/fixtures/markdown"
36
+ format: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typra/emitter",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Generic TypeSpec emitter for generating multi-runtime model surfaces",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,6 +59,7 @@
59
59
  "build": "tsc",
60
60
  "watch": "tsc --watch",
61
61
  "generate:fixtures": "tsp compile ./fixtures/shapes/main.tsp --config ./fixtures/tspconfig.yaml",
62
+ "validate:fixtures": "node ./scripts/validate-fixtures.mjs",
62
63
  "test": "node --test \"dist/test/*.test.js\"",
63
64
  "lint": "eslint src/ test/ --report-unused-disable-directives --max-warnings=0",
64
65
  "lint:fix": "eslint . --report-unused-disable-directives --fix",