@typra/emitter 0.2.0
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 +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- package/src/lib/main.tsp +110 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { TypraEmitterOptions } from "../lib.js";
|
|
3
|
+
export declare function emitGeneratedFile(context: EmitContext<TypraEmitterOptions>, filePath: string, content: string, options?: {
|
|
4
|
+
marker?: boolean;
|
|
5
|
+
}): Promise<void>;
|
|
6
|
+
export declare function emitGeneratedManifest(context: EmitContext<TypraEmitterOptions>): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { emitFile, resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { dirname, relative, resolve } from "path";
|
|
3
|
+
const generatedFilesByProgram = new WeakMap();
|
|
4
|
+
export async function emitGeneratedFile(context, filePath, content, options = {}) {
|
|
5
|
+
const marker = options.marker ?? shouldMark(filePath);
|
|
6
|
+
const finalContent = marker ? addMarker(filePath, content) : content;
|
|
7
|
+
recordGeneratedFile(context.program, filePath, marker);
|
|
8
|
+
await emitFile(context.program, {
|
|
9
|
+
path: filePath,
|
|
10
|
+
content: finalContent,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function emitGeneratedManifest(context) {
|
|
14
|
+
const entries = [...(generatedFilesByProgram.get(context.program)?.values() ?? [])]
|
|
15
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
16
|
+
const manifest = {
|
|
17
|
+
emitter: "typra-emitter",
|
|
18
|
+
version: 1,
|
|
19
|
+
generatedAt: new Date().toISOString(),
|
|
20
|
+
files: entries,
|
|
21
|
+
};
|
|
22
|
+
await emitFile(context.program, {
|
|
23
|
+
path: resolvePath(context.emitterOutputDir, ".typra-generated", "manifest.json"),
|
|
24
|
+
content: JSON.stringify(manifest, null, 2),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function recordGeneratedFile(program, filePath, marker) {
|
|
28
|
+
let entries = generatedFilesByProgram.get(program);
|
|
29
|
+
if (!entries) {
|
|
30
|
+
entries = new Map();
|
|
31
|
+
generatedFilesByProgram.set(program, entries);
|
|
32
|
+
}
|
|
33
|
+
entries.set(normalizePath(filePath), {
|
|
34
|
+
path: normalizePath(filePath),
|
|
35
|
+
marker,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function shouldMark(filePath) {
|
|
39
|
+
return !filePath.endsWith(".json");
|
|
40
|
+
}
|
|
41
|
+
function addMarker(filePath, content) {
|
|
42
|
+
const marker = markerFor(filePath);
|
|
43
|
+
return content.startsWith(marker) ? content : `${marker}\n${content}`;
|
|
44
|
+
}
|
|
45
|
+
function markerFor(filePath) {
|
|
46
|
+
if (filePath.endsWith(".md")) {
|
|
47
|
+
return "<!-- <auto-generated by typra-emitter> -->";
|
|
48
|
+
}
|
|
49
|
+
if (filePath.endsWith(".py") || filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
50
|
+
return "# <auto-generated by typra-emitter>";
|
|
51
|
+
}
|
|
52
|
+
return "// <auto-generated by typra-emitter>";
|
|
53
|
+
}
|
|
54
|
+
function normalizePath(filePath) {
|
|
55
|
+
const absolute = resolve(filePath);
|
|
56
|
+
const base = dirname(absolute);
|
|
57
|
+
return relative(base, absolute).startsWith("..")
|
|
58
|
+
? absolute.replace(/\\/g, "/")
|
|
59
|
+
: filePath.replace(/\\/g, "/");
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=generated-file.js.map
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generate } from "./generate.js";
|
|
3
|
+
import { parseArgs } from "util";
|
|
4
|
+
const HELP = `
|
|
5
|
+
typra-generate - Generate Typra runtime surfaces
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
npx typra-generate [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
-o, --output <dir> Output directory (required)
|
|
12
|
+
-t, --targets <list> Comma-separated list of targets (default: python,csharp,typescript,go)
|
|
13
|
+
-r, --root-object <name> Root object to generate from (default: Typra.FixtureRoot)
|
|
14
|
+
--omit <list> Comma-separated list of models to omit
|
|
15
|
+
-n, --namespace <name> Root namespace for generated code (default: Typra)
|
|
16
|
+
--no-tests Skip generating test files
|
|
17
|
+
--no-format Skip running formatters
|
|
18
|
+
-h, --help Show this help message
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
# Generate all runtimes to ./generated
|
|
22
|
+
npx typra-generate -o ./generated
|
|
23
|
+
|
|
24
|
+
# Generate only Python and C#
|
|
25
|
+
npx typra-generate -o ./lib -t python,csharp
|
|
26
|
+
|
|
27
|
+
# Generate a different root object
|
|
28
|
+
npx typra-generate -o ./lib -r Typra.Widget
|
|
29
|
+
|
|
30
|
+
# Omit specific models
|
|
31
|
+
npx typra-generate -o ./lib --omit LegacyWidget
|
|
32
|
+
|
|
33
|
+
Targets:
|
|
34
|
+
python Python dataclasses with YAML/JSON serialization
|
|
35
|
+
csharp C# classes with System.Text.Json serialization
|
|
36
|
+
typescript TypeScript interfaces with js-yaml serialization
|
|
37
|
+
go Go structs with encoding/json and gopkg.in/yaml.v3
|
|
38
|
+
markdown Markdown documentation
|
|
39
|
+
`;
|
|
40
|
+
async function main() {
|
|
41
|
+
const { values, positionals } = parseArgs({
|
|
42
|
+
options: {
|
|
43
|
+
output: { type: "string", short: "o" },
|
|
44
|
+
targets: { type: "string", short: "t" },
|
|
45
|
+
"root-object": { type: "string", short: "r" },
|
|
46
|
+
omit: { type: "string" },
|
|
47
|
+
namespace: { type: "string", short: "n" },
|
|
48
|
+
"no-tests": { type: "boolean", default: false },
|
|
49
|
+
"no-format": { type: "boolean", default: false },
|
|
50
|
+
help: { type: "boolean", short: "h" },
|
|
51
|
+
},
|
|
52
|
+
allowPositionals: true,
|
|
53
|
+
});
|
|
54
|
+
if (values.help) {
|
|
55
|
+
console.log(HELP);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
// Output is required
|
|
59
|
+
const output = values.output || positionals[0];
|
|
60
|
+
if (!output) {
|
|
61
|
+
console.error("Error: Output directory is required. Use -o <dir> or --output <dir>\n");
|
|
62
|
+
console.log(HELP);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
// Parse targets
|
|
66
|
+
const targetsString = values.targets || "python,csharp,typescript,go";
|
|
67
|
+
const targets = targetsString.split(",").map(t => t.trim().toLowerCase());
|
|
68
|
+
// Validate targets
|
|
69
|
+
const validTargets = ["python", "csharp", "typescript", "go", "markdown"];
|
|
70
|
+
for (const target of targets) {
|
|
71
|
+
if (!validTargets.includes(target)) {
|
|
72
|
+
console.error(`Error: Invalid target "${target}". Valid targets: ${validTargets.join(", ")}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Parse omit list
|
|
77
|
+
const omit = values.omit ? values.omit.split(",").map(m => m.trim()) : [];
|
|
78
|
+
console.log(`\n🚀 Typra Generator\n`);
|
|
79
|
+
console.log(` Output: ${output}`);
|
|
80
|
+
console.log(` Targets: ${targets.join(", ")}`);
|
|
81
|
+
console.log(` Root Object: ${values["root-object"] || "Typra.FixtureRoot"}`);
|
|
82
|
+
if (omit.length > 0) {
|
|
83
|
+
console.log(` Omitting: ${omit.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
const result = await generate({
|
|
87
|
+
output,
|
|
88
|
+
targets,
|
|
89
|
+
rootObject: values["root-object"] || "Typra.FixtureRoot",
|
|
90
|
+
omit,
|
|
91
|
+
namespace: values.namespace,
|
|
92
|
+
generateTests: !values["no-tests"],
|
|
93
|
+
format: !values["no-format"],
|
|
94
|
+
});
|
|
95
|
+
if (result.success) {
|
|
96
|
+
console.log(`✅ Successfully generated code for: ${result.targets.join(", ")}`);
|
|
97
|
+
console.log(` Output directory: ${result.outputDir}\n`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.error(`❌ Generation failed:`);
|
|
102
|
+
result.errors?.forEach(e => console.error(` - ${e}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
main().catch(error => {
|
|
107
|
+
console.error("Fatal error:", error);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type DecoratorContext, type Model, Program, Type, ModelProperty, ObjectValue } from "@typespec/compiler";
|
|
2
|
+
export declare const appendStateValue: <T>(context: DecoratorContext, key: symbol, target: Type, value: T | T[]) => void;
|
|
3
|
+
export declare const getStateValue: <T>(program: Program, key: symbol, target: Type) => T[];
|
|
4
|
+
export declare const setStateScalar: <T>(context: DecoratorContext, key: symbol, target: Type, value: T) => void;
|
|
5
|
+
export declare const getStateScalar: <T>(program: Program, key: symbol, target: Type) => T | undefined;
|
|
6
|
+
export interface SampleOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SampleEntry {
|
|
11
|
+
sample: object;
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function $sample(context: DecoratorContext, target: ModelProperty, sample: ObjectValue | object, options?: SampleOptions): void;
|
|
16
|
+
export declare function $abstract(context: DecoratorContext, target: Model): void;
|
|
17
|
+
export declare function $coerce(context: DecoratorContext, target: Model, scalar: Type, expansion: ObjectValue | object, title?: string, description?: string, example?: string): void;
|
|
18
|
+
export interface FactoryEntry {
|
|
19
|
+
/** Factory method name (e.g., "allow", "deny") */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Field assignments — { fieldName: value } */
|
|
22
|
+
sets: Record<string, any>;
|
|
23
|
+
/** Optional parameters — { paramName: typeString } */
|
|
24
|
+
params: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export interface MethodEntry {
|
|
27
|
+
/** Method name (e.g., "text") */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Return type as a string (e.g., "string") */
|
|
30
|
+
returns: string;
|
|
31
|
+
/** Human-readable description of what the method does */
|
|
32
|
+
description: string;
|
|
33
|
+
/** Method parameters as an ordered map of name → type string */
|
|
34
|
+
params: Record<string, string>;
|
|
35
|
+
/** Whether this method is optional (has a default implementation) */
|
|
36
|
+
optional: boolean;
|
|
37
|
+
/** Whether this method is synchronous (not wrapped in async/Promise/Task) */
|
|
38
|
+
sync: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function $factory(context: DecoratorContext, target: Model, name: string, sets: object, params?: object): void;
|
|
41
|
+
export declare function $method(context: DecoratorContext, target: Model, name: string, returns: string, description?: string, params?: object, optional?: boolean, sync?: boolean): void;
|
|
42
|
+
export interface KnownAsEntry {
|
|
43
|
+
/** Provider identifier (e.g., "openai", "anthropic") */
|
|
44
|
+
provider: string;
|
|
45
|
+
/** Wire field name for that provider */
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function $knownAs(context: DecoratorContext, target: ModelProperty, provider: string, name: string): void;
|
|
49
|
+
export interface DefaultForEntry {
|
|
50
|
+
/** Provider identifier (e.g., "openai", "anthropic") */
|
|
51
|
+
provider: string;
|
|
52
|
+
/** Default value for that provider */
|
|
53
|
+
defaultValue: any;
|
|
54
|
+
}
|
|
55
|
+
export declare function $defaultFor(context: DecoratorContext, target: ModelProperty, provider: string, defaultValue: ObjectValue | object | string | number | boolean): void;
|
|
56
|
+
export declare function $protocol(context: DecoratorContext, target: Model): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { serializeValueAsJson } from "@typespec/compiler";
|
|
2
|
+
import { StateKeys } from "./lib.js";
|
|
3
|
+
export const appendStateValue = (context, key, target, value) => {
|
|
4
|
+
const state = context.program.stateMap(key).get(target) || [];
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
const newState = [...state, ...value];
|
|
7
|
+
context.program.stateMap(key).set(target, newState);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const newState = [...state, value];
|
|
11
|
+
context.program.stateMap(key).set(target, newState);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const getStateValue = (program, key, target) => {
|
|
15
|
+
return program.stateMap(key).get(target) || [];
|
|
16
|
+
};
|
|
17
|
+
export const setStateScalar = (context, key, target, value) => {
|
|
18
|
+
context.program.stateMap(key).set(target, value);
|
|
19
|
+
};
|
|
20
|
+
export const getStateScalar = (program, key, target) => {
|
|
21
|
+
const value = program.stateMap(key).get(target);
|
|
22
|
+
return value ? value : undefined;
|
|
23
|
+
};
|
|
24
|
+
export function $sample(context, target, sample, options) {
|
|
25
|
+
// With valueof unknown, TypeSpec passes a plain JavaScript object
|
|
26
|
+
// With unknown (no valueof), TypeSpec passes an ObjectValue with a type property
|
|
27
|
+
let s;
|
|
28
|
+
if (sample && typeof sample === 'object' && 'type' in sample && sample.type) {
|
|
29
|
+
// Old-style ObjectValue with type property
|
|
30
|
+
const sampleValue = sample;
|
|
31
|
+
const serialized = serializeValueAsJson(context.program, sampleValue, sampleValue.type);
|
|
32
|
+
if (!serialized) {
|
|
33
|
+
context.program.reportDiagnostic({
|
|
34
|
+
code: "typra-emitter-sample-serialization",
|
|
35
|
+
message: `Failed to serialize sample value.`,
|
|
36
|
+
severity: "error",
|
|
37
|
+
target: sampleValue,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
s = serialized;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// New-style: plain JavaScript object from valueof unknown
|
|
45
|
+
s = sample;
|
|
46
|
+
}
|
|
47
|
+
if (!s.hasOwnProperty(target.name)) {
|
|
48
|
+
context.program.reportDiagnostic({
|
|
49
|
+
code: "typra-emitter-sample-name-mismatch",
|
|
50
|
+
message: `Sample object must have a property named '${target.name}' to match the target property.`,
|
|
51
|
+
severity: "error",
|
|
52
|
+
target: target,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const entry = {
|
|
57
|
+
sample: s,
|
|
58
|
+
title: options?.title ?? "",
|
|
59
|
+
description: options?.description ?? "",
|
|
60
|
+
};
|
|
61
|
+
appendStateValue(context, StateKeys.samples, target, entry);
|
|
62
|
+
}
|
|
63
|
+
export function $abstract(context, target) {
|
|
64
|
+
setStateScalar(context, StateKeys.abstracts, target, true);
|
|
65
|
+
}
|
|
66
|
+
export function $coerce(context, target, scalar, expansion, title, description, example) {
|
|
67
|
+
if (scalar.kind !== "Scalar") {
|
|
68
|
+
context.program.reportDiagnostic({
|
|
69
|
+
code: "typra-emitter-coerce-scalar-type",
|
|
70
|
+
message: `Coerce decorator requires a scalar type for the scalar representation.`,
|
|
71
|
+
severity: "error",
|
|
72
|
+
target: scalar,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Handle both ObjectValue (old style) and plain object (valueof unknown)
|
|
77
|
+
let exp;
|
|
78
|
+
if (expansion && typeof expansion === 'object' && 'type' in expansion && expansion.type) {
|
|
79
|
+
const serialized = serializeValueAsJson(context.program, expansion, expansion.type);
|
|
80
|
+
if (!serialized) {
|
|
81
|
+
context.program.reportDiagnostic({
|
|
82
|
+
code: "typra-emitter-coerce-serialization",
|
|
83
|
+
message: `Failed to serialize expansion value.`,
|
|
84
|
+
severity: "error",
|
|
85
|
+
target: target,
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
exp = serialized;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
exp = expansion;
|
|
93
|
+
}
|
|
94
|
+
// Handle string parameters that come as plain strings from valueof
|
|
95
|
+
const titleValue = typeof title === 'object' && title !== null && 'value' in title ? title.value : title;
|
|
96
|
+
const descValue = typeof description === 'object' && description !== null && 'value' in description ? description.value : description;
|
|
97
|
+
const exampleValue = typeof example === 'object' && example !== null && 'value' in example ? example.value : example;
|
|
98
|
+
const entry = {
|
|
99
|
+
scalar: scalar.name,
|
|
100
|
+
expansion: exp,
|
|
101
|
+
example: exampleValue,
|
|
102
|
+
title: titleValue ?? "",
|
|
103
|
+
description: descValue ?? "",
|
|
104
|
+
};
|
|
105
|
+
appendStateValue(context, StateKeys.coercions, target, entry);
|
|
106
|
+
}
|
|
107
|
+
function deserializeValue(value) {
|
|
108
|
+
if (value && typeof value === 'object' && 'type' in value && value.type) {
|
|
109
|
+
// ObjectValue from TypeSpec — shouldn't happen with valueof but handle defensively
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
export function $factory(context, target, name, sets, params) {
|
|
115
|
+
// Handle string values from valueof
|
|
116
|
+
const nameValue = typeof name === 'object' && name !== null && 'value' in name ? name.value : name;
|
|
117
|
+
const setsValue = deserializeValue(sets);
|
|
118
|
+
const paramsValue = params ? deserializeValue(params) : {};
|
|
119
|
+
const entry = {
|
|
120
|
+
name: nameValue,
|
|
121
|
+
sets: setsValue,
|
|
122
|
+
params: paramsValue,
|
|
123
|
+
};
|
|
124
|
+
appendStateValue(context, StateKeys.factories, target, entry);
|
|
125
|
+
}
|
|
126
|
+
export function $method(context, target, name, returns, description, params, optional, sync) {
|
|
127
|
+
const nameValue = typeof name === 'object' && name !== null && 'value' in name ? name.value : name;
|
|
128
|
+
const returnsValue = typeof returns === 'object' && returns !== null && 'value' in returns ? returns.value : returns;
|
|
129
|
+
const descValue = typeof description === 'object' && description !== null && 'value' in description ? description.value : description;
|
|
130
|
+
const paramsValue = params ? deserializeValue(params) : {};
|
|
131
|
+
const optionalValue = typeof optional === 'object' && optional !== null && 'value' in optional ? optional.value : optional ?? false;
|
|
132
|
+
const syncValue = typeof sync === 'object' && sync !== null && 'value' in sync ? sync.value : sync ?? false;
|
|
133
|
+
const entry = {
|
|
134
|
+
name: nameValue,
|
|
135
|
+
returns: returnsValue,
|
|
136
|
+
description: descValue ?? "",
|
|
137
|
+
params: paramsValue,
|
|
138
|
+
optional: optionalValue,
|
|
139
|
+
sync: syncValue,
|
|
140
|
+
};
|
|
141
|
+
appendStateValue(context, StateKeys.methods, target, entry);
|
|
142
|
+
}
|
|
143
|
+
export function $knownAs(context, target, provider, name) {
|
|
144
|
+
const providerValue = typeof provider === 'object' && provider !== null && 'value' in provider ? provider.value : provider;
|
|
145
|
+
const nameValue = typeof name === 'object' && name !== null && 'value' in name ? name.value : name;
|
|
146
|
+
const entry = { provider: providerValue, name: nameValue };
|
|
147
|
+
appendStateValue(context, StateKeys.knownAs, target, entry);
|
|
148
|
+
}
|
|
149
|
+
export function $defaultFor(context, target, provider, defaultValue) {
|
|
150
|
+
const providerValue = typeof provider === 'object' && provider !== null && 'value' in provider ? provider.value : provider;
|
|
151
|
+
let val;
|
|
152
|
+
if (defaultValue && typeof defaultValue === 'object' && 'type' in defaultValue && defaultValue.type) {
|
|
153
|
+
const serialized = serializeValueAsJson(context.program, defaultValue, defaultValue.type);
|
|
154
|
+
if (!serialized) {
|
|
155
|
+
context.program.reportDiagnostic({
|
|
156
|
+
code: "typra-emitter-defaultfor-serialization",
|
|
157
|
+
message: `Failed to serialize default value.`,
|
|
158
|
+
severity: "error",
|
|
159
|
+
target: target,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
val = serialized;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
val = defaultValue;
|
|
167
|
+
}
|
|
168
|
+
const entry = { provider: providerValue, defaultValue: val };
|
|
169
|
+
appendStateValue(context, StateKeys.defaultFor, target, entry);
|
|
170
|
+
}
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Protocol decorator
|
|
173
|
+
// ============================================================================
|
|
174
|
+
export function $protocol(context, target) {
|
|
175
|
+
setStateScalar(context, StateKeys.protocols, target, true);
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=decorators.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { TypeNode } from "./ir/ast.js";
|
|
3
|
+
import { TypraEmitterOptions } from "./lib.js";
|
|
4
|
+
export interface GeneratorOptions {
|
|
5
|
+
omitModels?: string[];
|
|
6
|
+
additionalModels?: TypeNode[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Filter nodes based on omit-models option.
|
|
10
|
+
* Matches against model name (e.g., "AgentManifest") or fully qualified name (e.g., "Prompty.AgentManifest")
|
|
11
|
+
*/
|
|
12
|
+
export declare function filterNodes(nodes: TypeNode[], options?: GeneratorOptions): TypeNode[];
|
|
13
|
+
export declare function $onEmit(context: EmitContext<TypraEmitterOptions>): Promise<void>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { resolveModel, enumerateTypes } from "./ir/ast.js";
|
|
3
|
+
import { generateMarkdown } from "./languages/markdown/driver.js";
|
|
4
|
+
import { generatePython } from "./languages/python/driver.js";
|
|
5
|
+
import { generateCsharp } from "./languages/csharp/driver.js";
|
|
6
|
+
import { generateTypeScript } from "./languages/typescript/driver.js";
|
|
7
|
+
import { generateGo } from "./languages/go/driver.js";
|
|
8
|
+
import { generateRust } from "./languages/rust/driver.js";
|
|
9
|
+
import { emitGeneratedFile, emitGeneratedManifest } from "./cleanup/generated-file.js";
|
|
10
|
+
/**
|
|
11
|
+
* Filter nodes based on omit-models option.
|
|
12
|
+
* Matches against model name (e.g., "AgentManifest") or fully qualified name (e.g., "Prompty.AgentManifest")
|
|
13
|
+
*/
|
|
14
|
+
export function filterNodes(nodes, options) {
|
|
15
|
+
const omitModels = options?.omitModels || [];
|
|
16
|
+
// Include additional root models and their type trees
|
|
17
|
+
const additionalModels = options?.additionalModels || [];
|
|
18
|
+
if (additionalModels.length > 0) {
|
|
19
|
+
const existingNames = new Set(nodes.map(n => `${n.typeName.namespace}.${n.typeName.name}`));
|
|
20
|
+
const visited = new Set(existingNames);
|
|
21
|
+
for (const additionalModel of additionalModels) {
|
|
22
|
+
for (const subNode of enumerateTypes(additionalModel, new Set())) {
|
|
23
|
+
const fullName = `${subNode.typeName.namespace}.${subNode.typeName.name}`;
|
|
24
|
+
if (!visited.has(fullName)) {
|
|
25
|
+
nodes.push(subNode);
|
|
26
|
+
visited.add(fullName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (omitModels.length === 0)
|
|
32
|
+
return nodes;
|
|
33
|
+
return nodes.filter(node => {
|
|
34
|
+
const name = node.typeName.name;
|
|
35
|
+
const fullName = `${node.typeName.namespace}.${name}`;
|
|
36
|
+
return !omitModels.includes(name) && !omitModels.includes(fullName);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Registry of available code generators
|
|
40
|
+
const generators = {
|
|
41
|
+
markdown: generateMarkdown,
|
|
42
|
+
python: generatePython,
|
|
43
|
+
csharp: generateCsharp,
|
|
44
|
+
typescript: generateTypeScript,
|
|
45
|
+
go: generateGo,
|
|
46
|
+
rust: generateRust,
|
|
47
|
+
};
|
|
48
|
+
export async function $onEmit(context) {
|
|
49
|
+
const options = {
|
|
50
|
+
emitterOutputDir: context.emitterOutputDir,
|
|
51
|
+
...context.options,
|
|
52
|
+
};
|
|
53
|
+
// resolving top level model
|
|
54
|
+
// this is the "Model" entry point for the emitter
|
|
55
|
+
const rootObject = options["root-object"];
|
|
56
|
+
const m = context.program.resolveTypeReference(rootObject);
|
|
57
|
+
if (!m[0] || m[0].kind !== "Model") {
|
|
58
|
+
throw new Error(`${rootObject} model not found or is not a model type.`);
|
|
59
|
+
}
|
|
60
|
+
const model = resolveModel(context.program, m[0], new Set(), options["root-namespace"] || "Typra", options["root-alias"] || "Typra");
|
|
61
|
+
if (options["root-alias"]) {
|
|
62
|
+
model.typeName = {
|
|
63
|
+
namespace: model.typeName.namespace,
|
|
64
|
+
name: options["root-alias"]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Discover additional models not reachable from the root.
|
|
68
|
+
// If root-namespace is specified, resolve all models in that namespace
|
|
69
|
+
// so new types are automatically emitted without manual additional-roots.
|
|
70
|
+
const rootNamespace = options["root-namespace"] || "Typra";
|
|
71
|
+
const additionalModels = [];
|
|
72
|
+
const visited = new Set();
|
|
73
|
+
// Collect names already in the main model tree to avoid duplicates
|
|
74
|
+
const collectNames = (node) => {
|
|
75
|
+
visited.add(`${node.typeName.namespace}.${node.typeName.name}`);
|
|
76
|
+
for (const child of node.childTypes) {
|
|
77
|
+
collectNames(child);
|
|
78
|
+
}
|
|
79
|
+
for (const prop of node.properties) {
|
|
80
|
+
if (prop.type) {
|
|
81
|
+
collectNames(prop.type);
|
|
82
|
+
for (const child of prop.type.childTypes) {
|
|
83
|
+
collectNames(child);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
collectNames(model);
|
|
89
|
+
// Resolve the namespace and iterate all models
|
|
90
|
+
const nsRef = context.program.resolveTypeReference(rootNamespace);
|
|
91
|
+
if (nsRef[0] && nsRef[0].kind === "Namespace") {
|
|
92
|
+
const ns = nsRef[0];
|
|
93
|
+
for (const [, nsModel] of ns.models) {
|
|
94
|
+
const fullName = `${rootNamespace}.${nsModel.name}`;
|
|
95
|
+
if (visited.has(fullName))
|
|
96
|
+
continue;
|
|
97
|
+
// Skip uninstantiated template declarations (e.g., Named<T>, Id<T>)
|
|
98
|
+
if (nsModel.node && 'templateParameters' in nsModel.node &&
|
|
99
|
+
nsModel.node.templateParameters.length > 0 && !nsModel.templateMapper) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const additionalNode = resolveModel(context.program, nsModel, new Set(), rootNamespace, options["root-alias"] || "Typra");
|
|
103
|
+
additionalModels.push(additionalNode);
|
|
104
|
+
visited.add(fullName);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Also process any explicit additional-roots (for types outside the namespace)
|
|
108
|
+
const additionalRoots = options["additional-roots"] || [];
|
|
109
|
+
for (const rootName of additionalRoots) {
|
|
110
|
+
if (visited.has(rootName))
|
|
111
|
+
continue;
|
|
112
|
+
const ref = context.program.resolveTypeReference(rootName);
|
|
113
|
+
if (!ref[0] || ref[0].kind !== "Model") {
|
|
114
|
+
console.warn(`Warning: additional-root '${rootName}' not found or is not a model type. Skipping.`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const additionalNode = resolveModel(context.program, ref[0], new Set(), rootNamespace, options["root-alias"] || "Typra");
|
|
118
|
+
additionalModels.push(additionalNode);
|
|
119
|
+
visited.add(rootName);
|
|
120
|
+
}
|
|
121
|
+
const targets = options["emit-targets"] || [];
|
|
122
|
+
const generatorOptions = {
|
|
123
|
+
omitModels: options["omit-models"] || [],
|
|
124
|
+
additionalModels: additionalModels,
|
|
125
|
+
};
|
|
126
|
+
// Dispatch to registered generators
|
|
127
|
+
for (const target of targets) {
|
|
128
|
+
const generatorName = target.type.toLowerCase().trim();
|
|
129
|
+
const generator = generators[generatorName];
|
|
130
|
+
if (generator) {
|
|
131
|
+
await generator(context, model, target, generatorOptions);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await emitGeneratedFile(context, resolvePath(context.emitterOutputDir, "json-ast", "model.json"), JSON.stringify(model.getSanitizedObject(), null, 2), { marker: false });
|
|
135
|
+
await emitGeneratedManifest(context);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=emitter.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target language for code generation.
|
|
3
|
+
*/
|
|
4
|
+
export type TargetLanguage = "python" | "csharp" | "typescript" | "go" | "rust" | "markdown";
|
|
5
|
+
/**
|
|
6
|
+
* Options for a specific target language.
|
|
7
|
+
*/
|
|
8
|
+
export interface TargetOptions {
|
|
9
|
+
/** Output directory for generated code */
|
|
10
|
+
outputDir: string;
|
|
11
|
+
/** Output directory for generated tests (optional) */
|
|
12
|
+
testDir?: string;
|
|
13
|
+
/** Override the namespace for generated code */
|
|
14
|
+
namespace?: string;
|
|
15
|
+
/** Run formatters on emitted files (default: true) */
|
|
16
|
+
format?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for the generate function.
|
|
20
|
+
*/
|
|
21
|
+
export interface GenerateOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Output directory for generated code.
|
|
24
|
+
* Each target will create a subdirectory (e.g., output/python, output/csharp)
|
|
25
|
+
*/
|
|
26
|
+
output: string;
|
|
27
|
+
/**
|
|
28
|
+
* Target languages to generate code for.
|
|
29
|
+
* @default ["python", "csharp", "typescript", "go"]
|
|
30
|
+
*/
|
|
31
|
+
targets?: TargetLanguage[] | Record<TargetLanguage, TargetOptions>;
|
|
32
|
+
/**
|
|
33
|
+
* Root object to start generation from.
|
|
34
|
+
* @default "Typra.FixtureRoot"
|
|
35
|
+
*/
|
|
36
|
+
rootObject?: string;
|
|
37
|
+
/**
|
|
38
|
+
* List of model names to omit from generation.
|
|
39
|
+
* Can be simple names (e.g., "Widget") or fully qualified (e.g., "Typra.Widget")
|
|
40
|
+
*/
|
|
41
|
+
omit?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Root namespace for the generated code.
|
|
44
|
+
* @default "Typra"
|
|
45
|
+
*/
|
|
46
|
+
namespace?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Alias for the root object in generated code.
|
|
49
|
+
*/
|
|
50
|
+
rootAlias?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Generate test files.
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
generateTests?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Run formatters on emitted files.
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
format?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Result of the generate function.
|
|
64
|
+
*/
|
|
65
|
+
export interface GenerateResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
outputDir: string;
|
|
68
|
+
targets: string[];
|
|
69
|
+
errors?: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Generate Typra runtime surfaces.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { generate } from '@typra/emitter/generate';
|
|
77
|
+
*
|
|
78
|
+
* await generate({
|
|
79
|
+
* output: './generated',
|
|
80
|
+
* targets: ['python', 'csharp'],
|
|
81
|
+
* rootObject: 'Typra.Widget',
|
|
82
|
+
* omit: ['LegacyWidget']
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function generate(options: GenerateOptions): Promise<GenerateResult>;
|