@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.
- package/dist/src/cleanup/generated-file.d.ts +1 -0
- package/dist/src/cleanup/generated-file.js +4 -7
- package/dist/src/contract-surface.d.ts +50 -0
- package/dist/src/contract-surface.js +176 -0
- package/dist/src/emitter.d.ts +1 -0
- package/dist/src/emitter.js +35 -7
- package/dist/src/ir/ast.js +0 -1
- package/dist/src/ir/lower.js +15 -1
- package/dist/src/languages/csharp/driver.js +5 -5
- package/dist/src/languages/csharp/emitter.js +14 -3
- package/dist/src/languages/go/driver.d.ts +1 -0
- package/dist/src/languages/go/driver.js +8 -6
- package/dist/src/languages/go/emitter.js +17 -14
- package/dist/src/languages/markdown/driver.js +1 -1
- package/dist/src/languages/python/driver.js +5 -5
- package/dist/src/languages/python/emitter.js +2 -0
- package/dist/src/languages/rust/driver.d.ts +11 -0
- package/dist/src/languages/rust/driver.js +8 -8
- package/dist/src/languages/rust/emitter.js +9 -5
- package/dist/src/languages/typescript/driver.js +5 -5
- package/dist/src/languages/typescript/emitter.js +2 -0
- package/dist/src/lib.d.ts +1 -0
- package/dist/src/lib.js +5 -0
- package/fixtures/shapes/main.tsp +62 -0
- package/fixtures/shapes/model/events/session.tsp +13 -0
- package/fixtures/shapes/model/pipeline/harness.tsp +11 -0
- package/fixtures/tspconfig.yaml +24 -1
- package/package.json +2 -1
|
@@ -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
|
-
|
|
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
|
package/dist/src/emitter.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/emitter.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
package/dist/src/ir/ast.js
CHANGED
|
@@ -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) {
|
package/dist/src/ir/lower.js
CHANGED
|
@@ -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([
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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"]
|
package/fixtures/shapes/main.tsp
CHANGED
|
@@ -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,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 {}
|
package/fixtures/tspconfig.yaml
CHANGED
|
@@ -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
|
+
"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",
|