@typra/emitter 0.2.4 → 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.
@@ -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
@@ -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")
@@ -145,6 +146,10 @@ export async function $onEmit(context) {
145
146
  omitModels: options["omit-models"] || [],
146
147
  additionalModels: additionalModels,
147
148
  };
149
+ const exportSurfaceNodes = filterNodes(Array.from(enumerateTypes(model)), {
150
+ omitModels: generatorOptions.omitModels,
151
+ additionalModels: [...additionalModels],
152
+ });
148
153
  // Dispatch to registered generators
149
154
  for (const target of targets) {
150
155
  const generatorName = target.type.toLowerCase().trim();
@@ -154,6 +159,7 @@ export async function $onEmit(context) {
154
159
  }
155
160
  }
156
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));
157
163
  await emitGeneratedManifest(context);
158
164
  }
159
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) {
@@ -990,16 +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 === "void")
999
- return "()";
1000
- if (method.returns === "string")
1001
- return "String";
1002
- return RUST_TYPE_MAP[method.returns] || method.returns;
1002
+ return protocolRustType(method.returns);
1003
1003
  }
1004
1004
  // ============================================================================
1005
1005
  // Protocol trait emission
@@ -5,27 +5,44 @@ import "./model/pipeline/harness.tsp";
5
5
  namespace Typra.Fixtures;
6
6
 
7
7
  model FixtureRoot {
8
+ @sample(#{ name: "fixture-root" })
8
9
  @doc("Required scalar field")
9
10
  name: string;
10
11
 
12
+ @sample(#{ description: "A generated fixture with broad emitter coverage." })
11
13
  @doc("Optional scalar field")
12
14
  description?: string;
13
15
 
16
+ @sample(#{ tags: #["typespec", "emitter", "validation"] })
14
17
  @doc("Array of scalar tags")
15
18
  tags: string[];
16
19
 
20
+ @sample(#{ metadata: #{ source: "fixture", version: 1 } })
17
21
  @doc("Dictionary-shaped metadata")
18
22
  metadata?: Record<unknown>;
19
23
 
24
+ @sample(#{ owner: #{ id: "owner-1", displayName: "Fixture Owner" } })
20
25
  @doc("Nested object field")
21
26
  owner: FixtureOwner;
22
27
 
28
+ @sample(#{ content: #{ kind: "text", text: "hello from a polymorphic sample" } })
23
29
  @doc("Discriminated union field")
24
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;
25
39
  }
26
40
 
27
41
  model FixtureOwner {
42
+ @sample(#{ id: "owner-1" })
28
43
  id: string;
44
+
45
+ @sample(#{ displayName: "Fixture Owner" })
29
46
  displayName?: string;
30
47
  }
31
48
 
@@ -36,10 +53,53 @@ model FixtureContent {
36
53
 
37
54
  model TextContent extends FixtureContent {
38
55
  kind: "text";
56
+
57
+ @sample(#{ text: "hello from text content" })
39
58
  text: string;
40
59
  }
41
60
 
42
61
  model ImageContent extends FixtureContent {
43
62
  kind: "image";
63
+
64
+ @sample(#{ url: "https://example.test/image.png" })
44
65
  url: string;
45
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
+ }
@@ -21,8 +21,16 @@ options:
21
21
  import-path: "fixtures"
22
22
  package-name: "fixtures"
23
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
24
29
  - type: Rust
25
30
  output-dir: "generated/fixtures/rust"
26
31
  test-dir: "generated/fixtures/rust/tests"
27
32
  import-path: "fixtures::model"
28
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.4",
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",