@typra/emitter 0.2.4 → 0.2.6
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/README.md +6 -1
- package/dist/src/compatibility.d.ts +32 -0
- package/dist/src/compatibility.js +96 -0
- package/dist/src/contract-surface.d.ts +52 -0
- package/dist/src/contract-surface.js +178 -0
- package/dist/src/emitter.js +11 -0
- package/dist/src/ir/ast.js +0 -1
- package/dist/src/languages/rust/emitter.js +6 -6
- package/dist/src/lib.d.ts +1 -0
- package/dist/src/lib.js +6 -0
- package/fixtures/shapes/main.tsp +60 -0
- package/fixtures/tspconfig.yaml +8 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -11,9 +11,14 @@ ship runtime service implementations or product-specific contracts.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```powershell
|
|
14
|
-
npm install --save-dev @typra/emitter @typespec/compiler
|
|
14
|
+
npm install --save-dev @typra/emitter @typespec/compiler@1.10.0 @typespec/json-schema@1.10.0
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Typra currently validates against TypeSpec compiler and JSON schema emitter
|
|
18
|
+
`1.10.0`. Unvalidated TypeSpec versions report a clear diagnostic during emit;
|
|
19
|
+
set `allow-unsupported-typespec-version: true` only when you intentionally accept
|
|
20
|
+
possible generated output churn.
|
|
21
|
+
|
|
17
22
|
## Configure TypeSpec
|
|
18
23
|
|
|
19
24
|
Add the emitter to `tspconfig.yaml`:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EmitContext, NoTarget } from "@typespec/compiler";
|
|
2
|
+
import { TypraEmitterOptions } from "./lib.js";
|
|
3
|
+
export declare const SUPPORTED_TYPESPEC_COMPILER_VERSION = "1.10.0";
|
|
4
|
+
export declare const SUPPORTED_TYPESPEC_JSON_SCHEMA_VERSION = "1.10.0";
|
|
5
|
+
export interface ToolchainPackageMetadata {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
supportedRange: string;
|
|
9
|
+
supported: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolchainMetadata {
|
|
12
|
+
packages: ToolchainPackageMetadata[];
|
|
13
|
+
}
|
|
14
|
+
interface CompatibilityDiagnosticContext {
|
|
15
|
+
options: Pick<TypraEmitterOptions, "allow-unsupported-typespec-version">;
|
|
16
|
+
program: {
|
|
17
|
+
reportDiagnostic(diagnostic: {
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
severity: "error" | "warning";
|
|
21
|
+
target: typeof NoTarget;
|
|
22
|
+
}): void;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function getToolchainMetadata(): ToolchainMetadata;
|
|
26
|
+
export declare function buildToolchainMetadata(packages: Array<Omit<ToolchainPackageMetadata, "supported"> & Partial<Pick<ToolchainPackageMetadata, "supported">>>): ToolchainMetadata;
|
|
27
|
+
export declare function reportTypeSpecCompatibility(context: EmitContext<TypraEmitterOptions>): ToolchainMetadata;
|
|
28
|
+
export declare function shouldBlockUnsupportedTypeSpecToolchain(options: Pick<TypraEmitterOptions, "allow-unsupported-typespec-version">, toolchain: ToolchainMetadata): boolean;
|
|
29
|
+
export declare function reportToolchainCompatibility(context: CompatibilityDiagnosticContext, toolchain: ToolchainMetadata): void;
|
|
30
|
+
export declare function getUnsupportedTypeSpecPackages(toolchain: ToolchainMetadata): ToolchainPackageMetadata[];
|
|
31
|
+
export declare function formatUnsupportedTypeSpecVersionMessage(unsupported: ToolchainPackageMetadata[], allowedUnsupportedVersion: boolean): string;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { NoTarget } from "@typespec/compiler";
|
|
6
|
+
export const SUPPORTED_TYPESPEC_COMPILER_VERSION = "1.10.0";
|
|
7
|
+
export const SUPPORTED_TYPESPEC_JSON_SCHEMA_VERSION = "1.10.0";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
export function getToolchainMetadata() {
|
|
10
|
+
const emitterVersion = resolveNearestPackageVersion("@typra/emitter", dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
return buildToolchainMetadata([
|
|
12
|
+
{
|
|
13
|
+
name: "@typespec/compiler",
|
|
14
|
+
version: resolveInstalledPackageVersion("@typespec/compiler"),
|
|
15
|
+
supportedRange: SUPPORTED_TYPESPEC_COMPILER_VERSION,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "@typespec/json-schema",
|
|
19
|
+
version: resolveInstalledPackageVersion("@typespec/json-schema"),
|
|
20
|
+
supportedRange: SUPPORTED_TYPESPEC_JSON_SCHEMA_VERSION,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "@typra/emitter",
|
|
24
|
+
version: emitterVersion,
|
|
25
|
+
supportedRange: emitterVersion,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
export function buildToolchainMetadata(packages) {
|
|
30
|
+
return {
|
|
31
|
+
packages: packages
|
|
32
|
+
.map((entry) => ({
|
|
33
|
+
...entry,
|
|
34
|
+
supported: entry.supported ?? isSupportedVersion(entry.version, entry.supportedRange),
|
|
35
|
+
}))
|
|
36
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function reportTypeSpecCompatibility(context) {
|
|
40
|
+
const toolchain = getToolchainMetadata();
|
|
41
|
+
reportToolchainCompatibility(context, toolchain);
|
|
42
|
+
return toolchain;
|
|
43
|
+
}
|
|
44
|
+
export function shouldBlockUnsupportedTypeSpecToolchain(options, toolchain) {
|
|
45
|
+
return options["allow-unsupported-typespec-version"] !== true && getUnsupportedTypeSpecPackages(toolchain).length > 0;
|
|
46
|
+
}
|
|
47
|
+
export function reportToolchainCompatibility(context, toolchain) {
|
|
48
|
+
const unsupported = getUnsupportedTypeSpecPackages(toolchain);
|
|
49
|
+
if (unsupported.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
context.program.reportDiagnostic({
|
|
53
|
+
code: "typra-emitter-unsupported-typespec-version",
|
|
54
|
+
message: formatUnsupportedTypeSpecVersionMessage(unsupported, context.options["allow-unsupported-typespec-version"] === true),
|
|
55
|
+
severity: context.options["allow-unsupported-typespec-version"] === true ? "warning" : "error",
|
|
56
|
+
target: NoTarget,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function getUnsupportedTypeSpecPackages(toolchain) {
|
|
60
|
+
return toolchain.packages.filter((entry) => (entry.name === "@typespec/compiler" || entry.name === "@typespec/json-schema") && !entry.supported);
|
|
61
|
+
}
|
|
62
|
+
export function formatUnsupportedTypeSpecVersionMessage(unsupported, allowedUnsupportedVersion) {
|
|
63
|
+
const actual = unsupported.map((entry) => `${entry.name}@${entry.version}`).join(", ");
|
|
64
|
+
const expected = unsupported.map((entry) => `${entry.name}@${entry.supportedRange}`).join(", ");
|
|
65
|
+
const action = allowedUnsupportedVersion
|
|
66
|
+
? "Generation will continue because allow-unsupported-typespec-version is enabled."
|
|
67
|
+
: "Pin the TypeSpec toolchain to the supported versions, or set allow-unsupported-typespec-version: true to continue with a warning.";
|
|
68
|
+
return `@typra/emitter has only been validated with ${expected}; found ${actual}. ${action}`;
|
|
69
|
+
}
|
|
70
|
+
function isSupportedVersion(version, supportedRange) {
|
|
71
|
+
return version === supportedRange;
|
|
72
|
+
}
|
|
73
|
+
function resolveInstalledPackageVersion(packageName) {
|
|
74
|
+
try {
|
|
75
|
+
const packageEntry = require.resolve(packageName);
|
|
76
|
+
return resolveNearestPackageVersion(packageName, dirname(packageEntry));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return "unresolved";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolveNearestPackageVersion(expectedPackageName, startDir) {
|
|
83
|
+
let current = startDir;
|
|
84
|
+
while (current !== dirname(current)) {
|
|
85
|
+
const candidate = join(current, "package.json");
|
|
86
|
+
if (existsSync(candidate)) {
|
|
87
|
+
const metadata = JSON.parse(readFileSync(candidate, "utf8"));
|
|
88
|
+
if (metadata.name === expectedPackageName && typeof metadata.version === "string") {
|
|
89
|
+
return metadata.version;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
current = dirname(current);
|
|
93
|
+
}
|
|
94
|
+
return "unresolved";
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=compatibility.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { EmitTarget, TypraEmitterOptions } from "./lib.js";
|
|
3
|
+
import { TypeNode } from "./ir/ast.js";
|
|
4
|
+
import { ToolchainMetadata } from "./compatibility.js";
|
|
5
|
+
export interface ExportSurfaceMethod {
|
|
6
|
+
name: string;
|
|
7
|
+
returns: string;
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
optional: boolean;
|
|
10
|
+
sync: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ExportSurfaceProtocol {
|
|
13
|
+
name: string;
|
|
14
|
+
group: string;
|
|
15
|
+
methods: ExportSurfaceMethod[];
|
|
16
|
+
}
|
|
17
|
+
export interface ExportSurfaceEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
kind: "type" | "value";
|
|
20
|
+
group: string;
|
|
21
|
+
source: string;
|
|
22
|
+
protocol: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ExportSurfaceGroup {
|
|
25
|
+
name: string;
|
|
26
|
+
exports: string[];
|
|
27
|
+
modules: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface TargetExportSurface {
|
|
30
|
+
target: string;
|
|
31
|
+
outputRoot: string;
|
|
32
|
+
packageName?: string;
|
|
33
|
+
namespace?: string;
|
|
34
|
+
rootExports: string[];
|
|
35
|
+
exports: ExportSurfaceEntry[];
|
|
36
|
+
groups: ExportSurfaceGroup[];
|
|
37
|
+
protocols: ExportSurfaceProtocol[];
|
|
38
|
+
modules: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface ExportSurfaceSnapshot {
|
|
41
|
+
emitter: "typra-emitter";
|
|
42
|
+
version: 1;
|
|
43
|
+
toolchain: ToolchainMetadata;
|
|
44
|
+
root: {
|
|
45
|
+
object: string;
|
|
46
|
+
namespace: string;
|
|
47
|
+
alias: string;
|
|
48
|
+
};
|
|
49
|
+
targets: TargetExportSurface[];
|
|
50
|
+
}
|
|
51
|
+
export declare function buildExportSurfaceSnapshot(rootObject: string, rootNamespace: string, rootAlias: string, targets: EmitTarget[], nodes: TypeNode[], toolchain?: ToolchainMetadata): ExportSurfaceSnapshot;
|
|
52
|
+
export declare function emitExportSurfaceSnapshot(context: EmitContext<TypraEmitterOptions>, snapshot: ExportSurfaceSnapshot): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { emitFile, resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { toKebabCase, toSnakeCase } from "./ir/utilities.js";
|
|
3
|
+
import { getToolchainMetadata } from "./compatibility.js";
|
|
4
|
+
export function buildExportSurfaceSnapshot(rootObject, rootNamespace, rootAlias, targets, nodes, toolchain = getToolchainMetadata()) {
|
|
5
|
+
return {
|
|
6
|
+
emitter: "typra-emitter",
|
|
7
|
+
version: 1,
|
|
8
|
+
toolchain,
|
|
9
|
+
root: {
|
|
10
|
+
object: rootObject,
|
|
11
|
+
namespace: rootNamespace,
|
|
12
|
+
alias: rootAlias,
|
|
13
|
+
},
|
|
14
|
+
targets: targets
|
|
15
|
+
.map((target) => buildTargetSurface(rootNamespace, target, nodes))
|
|
16
|
+
.sort((left, right) => left.target.localeCompare(right.target)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function emitExportSurfaceSnapshot(context, snapshot) {
|
|
20
|
+
await emitFile(context.program, {
|
|
21
|
+
path: resolvePath(context.emitterOutputDir, ".typra-generated", "export-surfaces.json"),
|
|
22
|
+
content: `${JSON.stringify(snapshot, null, 2)}\n`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function buildTargetSurface(rootNamespace, target, nodes) {
|
|
26
|
+
const targetName = normalizeTarget(target.type);
|
|
27
|
+
const baseTypes = nodes.filter((node) => !node.base).sort(compareNodes);
|
|
28
|
+
const groups = buildGroups(targetName, baseTypes);
|
|
29
|
+
const exports = buildExports(targetName, baseTypes);
|
|
30
|
+
const rootExports = uniqueSorted(exports.map((entry) => entry.name));
|
|
31
|
+
const protocols = nodes
|
|
32
|
+
.filter((node) => node.isProtocol)
|
|
33
|
+
.sort(compareNodes)
|
|
34
|
+
.map((node) => ({
|
|
35
|
+
name: node.typeName.name,
|
|
36
|
+
group: node.group || "",
|
|
37
|
+
methods: node.methods
|
|
38
|
+
.map((method) => ({
|
|
39
|
+
name: method.name,
|
|
40
|
+
returns: method.returns,
|
|
41
|
+
params: sortRecord(method.params),
|
|
42
|
+
optional: method.optional,
|
|
43
|
+
sync: method.sync,
|
|
44
|
+
}))
|
|
45
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
46
|
+
}));
|
|
47
|
+
return {
|
|
48
|
+
target: targetName,
|
|
49
|
+
outputRoot: target["output-dir"] || targetName,
|
|
50
|
+
...targetMetadata(rootNamespace, targetName, target),
|
|
51
|
+
rootExports,
|
|
52
|
+
exports,
|
|
53
|
+
groups,
|
|
54
|
+
protocols,
|
|
55
|
+
modules: buildModules(targetName, baseTypes),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function buildExports(targetName, baseTypes) {
|
|
59
|
+
return baseTypes
|
|
60
|
+
.flatMap((node) => {
|
|
61
|
+
const group = node.group || "";
|
|
62
|
+
const source = sourceFor(targetName, node, group);
|
|
63
|
+
const kind = node.isProtocol ? "type" : "value";
|
|
64
|
+
return [node, ...node.childTypes].map((exportedNode) => ({
|
|
65
|
+
name: exportedNode.typeName.name,
|
|
66
|
+
kind,
|
|
67
|
+
group,
|
|
68
|
+
source,
|
|
69
|
+
protocol: node.isProtocol,
|
|
70
|
+
}));
|
|
71
|
+
})
|
|
72
|
+
.sort((left, right) => {
|
|
73
|
+
const byGroup = left.group.localeCompare(right.group);
|
|
74
|
+
if (byGroup !== 0)
|
|
75
|
+
return byGroup;
|
|
76
|
+
return left.name.localeCompare(right.name);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function buildGroups(targetName, baseTypes) {
|
|
80
|
+
const groupMap = new Map();
|
|
81
|
+
for (const node of baseTypes) {
|
|
82
|
+
const group = node.group || "";
|
|
83
|
+
if (!group)
|
|
84
|
+
continue;
|
|
85
|
+
if (!groupMap.has(group))
|
|
86
|
+
groupMap.set(group, []);
|
|
87
|
+
groupMap.get(group).push(node);
|
|
88
|
+
}
|
|
89
|
+
return Array.from(groupMap.entries())
|
|
90
|
+
.map(([name, groupNodes]) => ({
|
|
91
|
+
name,
|
|
92
|
+
exports: uniqueSorted(groupNodes.flatMap((node) => [node.typeName.name, ...node.childTypes.map((child) => child.typeName.name)])),
|
|
93
|
+
modules: uniqueSorted(groupNodes.map((node) => groupModuleName(targetName, node))),
|
|
94
|
+
}))
|
|
95
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
96
|
+
}
|
|
97
|
+
function buildModules(targetName, baseTypes) {
|
|
98
|
+
if (targetName === "rust") {
|
|
99
|
+
return uniqueSorted(["context", ...baseTypes.map((node) => node.group || moduleName(node))]);
|
|
100
|
+
}
|
|
101
|
+
return uniqueSorted(baseTypes.map((node) => sourceFor(targetName, node, node.group || "")));
|
|
102
|
+
}
|
|
103
|
+
function targetMetadata(rootNamespace, targetName, target) {
|
|
104
|
+
if (targetName === "go") {
|
|
105
|
+
return {
|
|
106
|
+
packageName: target["package-name"] || goPackageNameFromNamespace(rootNamespace),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (targetName === "csharp") {
|
|
110
|
+
return {
|
|
111
|
+
namespace: target.namespace || rootNamespace,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (targetName === "typescript") {
|
|
115
|
+
return {
|
|
116
|
+
namespace: target.namespace || rootNamespace.replace(/\.Core$/, ""),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (targetName === "python") {
|
|
120
|
+
return {
|
|
121
|
+
packageName: rootNamespace.toLowerCase(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
function sourceFor(targetName, node, group) {
|
|
127
|
+
const name = node.typeName.name;
|
|
128
|
+
switch (targetName) {
|
|
129
|
+
case "typescript":
|
|
130
|
+
return group ? `./${group}/${toKebabCase(name)}` : `./${toKebabCase(name)}`;
|
|
131
|
+
case "python":
|
|
132
|
+
return group ? `.${group}` : `._${name}`;
|
|
133
|
+
case "rust":
|
|
134
|
+
return group ? `${group}::${toSnakeCase(name)}` : toSnakeCase(name);
|
|
135
|
+
case "go":
|
|
136
|
+
return `${toSnakeCase(name)}.go`;
|
|
137
|
+
case "csharp":
|
|
138
|
+
return group ? `${group}/${name}.cs` : `${name}.cs`;
|
|
139
|
+
default:
|
|
140
|
+
return name;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function moduleName(node) {
|
|
144
|
+
return toSnakeCase(node.typeName.name);
|
|
145
|
+
}
|
|
146
|
+
function groupModuleName(targetName, node) {
|
|
147
|
+
switch (targetName) {
|
|
148
|
+
case "typescript":
|
|
149
|
+
return toKebabCase(node.typeName.name);
|
|
150
|
+
case "python":
|
|
151
|
+
return `_${node.typeName.name}`;
|
|
152
|
+
case "csharp":
|
|
153
|
+
return `${node.typeName.name}.cs`;
|
|
154
|
+
default:
|
|
155
|
+
return moduleName(node);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function normalizeTarget(target) {
|
|
159
|
+
return target.toLowerCase().trim();
|
|
160
|
+
}
|
|
161
|
+
function goPackageNameFromNamespace(namespace) {
|
|
162
|
+
return namespace.toLowerCase().replace(/\./g, "");
|
|
163
|
+
}
|
|
164
|
+
function compareNodes(left, right) {
|
|
165
|
+
const leftGroup = left.group || "";
|
|
166
|
+
const rightGroup = right.group || "";
|
|
167
|
+
const byGroup = leftGroup.localeCompare(rightGroup);
|
|
168
|
+
if (byGroup !== 0)
|
|
169
|
+
return byGroup;
|
|
170
|
+
return left.typeName.name.localeCompare(right.typeName.name);
|
|
171
|
+
}
|
|
172
|
+
function uniqueSorted(values) {
|
|
173
|
+
return Array.from(new Set(values)).sort();
|
|
174
|
+
}
|
|
175
|
+
function sortRecord(record) {
|
|
176
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=contract-surface.js.map
|
package/dist/src/emitter.js
CHANGED
|
@@ -7,6 +7,8 @@ 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";
|
|
11
|
+
import { reportTypeSpecCompatibility, shouldBlockUnsupportedTypeSpecToolchain } from "./compatibility.js";
|
|
10
12
|
/**
|
|
11
13
|
* Filter nodes based on omit-models option.
|
|
12
14
|
* Matches against model name (e.g., "AgentManifest") or fully qualified name (e.g., "Prompty.AgentManifest")
|
|
@@ -68,6 +70,10 @@ const generators = {
|
|
|
68
70
|
rust: generateRust,
|
|
69
71
|
};
|
|
70
72
|
export async function $onEmit(context) {
|
|
73
|
+
const toolchain = reportTypeSpecCompatibility(context);
|
|
74
|
+
if (shouldBlockUnsupportedTypeSpecToolchain(context.options, toolchain)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
71
77
|
const options = {
|
|
72
78
|
emitterOutputDir: context.emitterOutputDir,
|
|
73
79
|
...context.options,
|
|
@@ -145,6 +151,10 @@ export async function $onEmit(context) {
|
|
|
145
151
|
omitModels: options["omit-models"] || [],
|
|
146
152
|
additionalModels: additionalModels,
|
|
147
153
|
};
|
|
154
|
+
const exportSurfaceNodes = filterNodes(Array.from(enumerateTypes(model)), {
|
|
155
|
+
omitModels: generatorOptions.omitModels,
|
|
156
|
+
additionalModels: [...additionalModels],
|
|
157
|
+
});
|
|
148
158
|
// Dispatch to registered generators
|
|
149
159
|
for (const target of targets) {
|
|
150
160
|
const generatorName = target.type.toLowerCase().trim();
|
|
@@ -154,6 +164,7 @@ export async function $onEmit(context) {
|
|
|
154
164
|
}
|
|
155
165
|
}
|
|
156
166
|
await emitGeneratedFile(context, resolvePath(context.emitterOutputDir, "json-ast", "model.json"), JSON.stringify(model.getSanitizedObject(), null, 2), { marker: false });
|
|
167
|
+
await emitExportSurfaceSnapshot(context, buildExportSurfaceSnapshot(rootObject, rootNamespace, rootAlias, targets, exportSurfaceNodes, toolchain));
|
|
157
168
|
await emitGeneratedManifest(context);
|
|
158
169
|
}
|
|
159
170
|
//# 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) {
|
|
@@ -990,16 +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 "()";
|
|
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
|
package/dist/src/lib.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface TypraEmitterOptions {
|
|
|
18
18
|
"omit-models"?: string[];
|
|
19
19
|
"schema-output-dir"?: string;
|
|
20
20
|
"additional-roots"?: string[];
|
|
21
|
+
"allow-unsupported-typespec-version"?: boolean;
|
|
21
22
|
}
|
|
22
23
|
export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
23
24
|
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
package/dist/src/lib.js
CHANGED
|
@@ -82,6 +82,12 @@ const TypraEmitterOptionsSchema = {
|
|
|
82
82
|
items: { type: "string" },
|
|
83
83
|
nullable: true,
|
|
84
84
|
description: "Additional root types to resolve and generate alongside the main root object. These types need not be referenced from the main root. Specified as fully-qualified names (e.g., 'Typra.Message')."
|
|
85
|
+
},
|
|
86
|
+
"allow-unsupported-typespec-version": {
|
|
87
|
+
type: "boolean",
|
|
88
|
+
nullable: true,
|
|
89
|
+
default: false,
|
|
90
|
+
description: "Allow generation with an unvalidated TypeSpec compiler/json-schema version. Unsupported versions report a warning instead of an error."
|
|
85
91
|
}
|
|
86
92
|
},
|
|
87
93
|
required: ["root-object"],
|
package/fixtures/shapes/main.tsp
CHANGED
|
@@ -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
|
+
}
|
package/fixtures/tspconfig.yaml
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Generic TypeSpec emitter for generating multi-runtime model surfaces",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
"fixtures/tspconfig.yaml"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@typespec/compiler": "
|
|
44
|
-
"@typespec/json-schema": "
|
|
43
|
+
"@typespec/compiler": "1.10.0",
|
|
44
|
+
"@typespec/json-schema": "1.10.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^24.7.0",
|
|
48
48
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
49
49
|
"@typescript-eslint/parser": "^8.15.0",
|
|
50
|
-
"@typespec/compiler": "
|
|
51
|
-
"@typespec/json-schema": "
|
|
50
|
+
"@typespec/compiler": "1.10.0",
|
|
51
|
+
"@typespec/json-schema": "1.10.0",
|
|
52
52
|
"eslint": "^9.15.0",
|
|
53
53
|
"markdownlint-cli": "^0.48.0",
|
|
54
54
|
"prettier": "^3.3.3",
|
|
@@ -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",
|