@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript scaffolding emitter — static/structural files.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the Nunjucks templates:
|
|
5
|
+
* - `context.ts.njk` → emitTypeScriptContext()
|
|
6
|
+
* - `index.ts.njk` → emitTypeScriptIndex()
|
|
7
|
+
* - `eslint.config.js.njk` → emitEslintConfig()
|
|
8
|
+
*
|
|
9
|
+
* These emit files whose content depends only on the type graph
|
|
10
|
+
* shape (not on the Declaration IR used for per-type files).
|
|
11
|
+
*/
|
|
12
|
+
import { TypeNode } from "../../ir/ast.js";
|
|
13
|
+
/**
|
|
14
|
+
* Emit static TypeScript context classes (LoadContext, SaveContext).
|
|
15
|
+
*/
|
|
16
|
+
export declare function emitTypeScriptContext(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Emit the barrel export index.ts file.
|
|
19
|
+
*
|
|
20
|
+
* When types are organised into group subfolders, the root index.ts re-exports
|
|
21
|
+
* from each group's index.ts (e.g. `export * from "./connection/index"`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function emitTypeScriptIndex(baseTypes: TypeNode[], _types: TypeNode[]): string;
|
|
24
|
+
/**
|
|
25
|
+
* Emit a group-level index.ts barrel file.
|
|
26
|
+
* Lives at `model/{group}/index.ts` and re-exports all types in that group.
|
|
27
|
+
*/
|
|
28
|
+
export declare function emitTypeScriptGroupIndex(group: string, groupNodes: TypeNode[]): string;
|
|
29
|
+
/**
|
|
30
|
+
* Emit the static ESLint configuration file.
|
|
31
|
+
*/
|
|
32
|
+
export declare function emitEslintConfig(): string;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript scaffolding emitter — static/structural files.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the Nunjucks templates:
|
|
5
|
+
* - `context.ts.njk` → emitTypeScriptContext()
|
|
6
|
+
* - `index.ts.njk` → emitTypeScriptIndex()
|
|
7
|
+
* - `eslint.config.js.njk` → emitEslintConfig()
|
|
8
|
+
*
|
|
9
|
+
* These emit files whose content depends only on the type graph
|
|
10
|
+
* shape (not on the Declaration IR used for per-type files).
|
|
11
|
+
*/
|
|
12
|
+
import { toKebabCase } from "../../ir/utilities.js";
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Context classes (LoadContext, SaveContext)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Emit static TypeScript context classes (LoadContext, SaveContext).
|
|
18
|
+
*/
|
|
19
|
+
export function emitTypeScriptContext() {
|
|
20
|
+
return `// Copyright (c) Microsoft. All rights reserved.
|
|
21
|
+
// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY.
|
|
22
|
+
|
|
23
|
+
import * as yaml from "yaml";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context for customizing the loading process of agent definitions.
|
|
27
|
+
*
|
|
28
|
+
* Provides hooks for pre-processing input data before parsing and
|
|
29
|
+
* post-processing output data after instantiation.
|
|
30
|
+
*/
|
|
31
|
+
export class LoadContext {
|
|
32
|
+
/**
|
|
33
|
+
* Optional callback to transform input data before parsing.
|
|
34
|
+
*/
|
|
35
|
+
preProcess?: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional callback to transform the result after instantiation.
|
|
39
|
+
*/
|
|
40
|
+
postProcess?: (result: unknown) => unknown;
|
|
41
|
+
|
|
42
|
+
constructor(init?: Partial<LoadContext>) {
|
|
43
|
+
if (init?.preProcess) {
|
|
44
|
+
this.preProcess = init.preProcess;
|
|
45
|
+
}
|
|
46
|
+
if (init?.postProcess) {
|
|
47
|
+
this.postProcess = init.postProcess;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply pre-processing to input data if a preProcess callback is set.
|
|
53
|
+
* @param data - The raw input dictionary to process.
|
|
54
|
+
* @returns The processed dictionary, or the original if no callback is set.
|
|
55
|
+
*/
|
|
56
|
+
processInput(data: Record<string, unknown>): Record<string, unknown> {
|
|
57
|
+
if (this.preProcess) {
|
|
58
|
+
return this.preProcess(data);
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply post-processing to the result if a postProcess callback is set.
|
|
65
|
+
* @param result - The instantiated object to process.
|
|
66
|
+
* @returns The processed result, or the original if no callback is set.
|
|
67
|
+
*/
|
|
68
|
+
processOutput<T>(result: T): T {
|
|
69
|
+
if (this.postProcess) {
|
|
70
|
+
return this.postProcess(result) as T;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Context for customizing the serialization process of agent definitions.
|
|
78
|
+
*
|
|
79
|
+
* Provides hooks for pre-processing the object before serialization and
|
|
80
|
+
* post-processing the dictionary after serialization.
|
|
81
|
+
*/
|
|
82
|
+
export class SaveContext {
|
|
83
|
+
/**
|
|
84
|
+
* Optional callback to transform the object before serialization.
|
|
85
|
+
*/
|
|
86
|
+
preSave?: (obj: unknown) => unknown;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Optional callback to transform the dictionary after serialization.
|
|
90
|
+
*/
|
|
91
|
+
postSave?: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Output format for collections: "object" (name as key) or "array" (list of dicts).
|
|
95
|
+
* Defaults to "object".
|
|
96
|
+
*/
|
|
97
|
+
collectionFormat: "object" | "array" = "object";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Use shorthand scalar representation when possible.
|
|
101
|
+
* Defaults to true.
|
|
102
|
+
*/
|
|
103
|
+
useShorthand: boolean = true;
|
|
104
|
+
|
|
105
|
+
constructor(init?: Partial<SaveContext>) {
|
|
106
|
+
if (init?.preSave) {
|
|
107
|
+
this.preSave = init.preSave;
|
|
108
|
+
}
|
|
109
|
+
if (init?.postSave) {
|
|
110
|
+
this.postSave = init.postSave;
|
|
111
|
+
}
|
|
112
|
+
if (init?.collectionFormat) {
|
|
113
|
+
this.collectionFormat = init.collectionFormat;
|
|
114
|
+
}
|
|
115
|
+
if (init?.useShorthand !== undefined) {
|
|
116
|
+
this.useShorthand = init.useShorthand;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Apply pre-processing to the object if a preSave callback is set.
|
|
122
|
+
* @param obj - The object to process before serialization.
|
|
123
|
+
* @returns The processed object, or the original if no callback is set.
|
|
124
|
+
*/
|
|
125
|
+
processObject<T>(obj: T): T {
|
|
126
|
+
if (this.preSave) {
|
|
127
|
+
return this.preSave(obj) as T;
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply post-processing to the dictionary if a postSave callback is set.
|
|
134
|
+
* @param data - The serialized dictionary to process.
|
|
135
|
+
* @returns The processed dictionary, or the original if no callback is set.
|
|
136
|
+
*/
|
|
137
|
+
processDict(data: Record<string, unknown>): Record<string, unknown> {
|
|
138
|
+
if (this.postSave) {
|
|
139
|
+
return this.postSave(data);
|
|
140
|
+
}
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Convert a dictionary to a YAML string.
|
|
146
|
+
* @param data - The dictionary to convert.
|
|
147
|
+
* @returns The YAML string representation.
|
|
148
|
+
*/
|
|
149
|
+
toYaml(data: Record<string, unknown>): string {
|
|
150
|
+
return yaml.stringify(data, { indent: 2 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert a dictionary to a JSON string.
|
|
155
|
+
* @param data - The dictionary to convert.
|
|
156
|
+
* @param indent - Number of spaces for indentation.
|
|
157
|
+
* @returns The JSON string representation.
|
|
158
|
+
*/
|
|
159
|
+
toJson(data: Record<string, unknown>, indent: number = 2): string {
|
|
160
|
+
return JSON.stringify(data, null, indent);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Barrel export index.ts
|
|
167
|
+
// ============================================================================
|
|
168
|
+
/**
|
|
169
|
+
* Emit the barrel export index.ts file.
|
|
170
|
+
*
|
|
171
|
+
* When types are organised into group subfolders, the root index.ts re-exports
|
|
172
|
+
* from each group's index.ts (e.g. `export * from "./connection/index"`).
|
|
173
|
+
*/
|
|
174
|
+
export function emitTypeScriptIndex(baseTypes, _types) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push("// Copyright (c) Microsoft. All rights reserved.");
|
|
177
|
+
lines.push("// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY.");
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push('export { LoadContext, SaveContext } from "./context";');
|
|
180
|
+
// Group root types by their semantic group
|
|
181
|
+
const groupMap = new Map();
|
|
182
|
+
for (const type of baseTypes) {
|
|
183
|
+
const g = type.group || "";
|
|
184
|
+
if (!groupMap.has(g))
|
|
185
|
+
groupMap.set(g, []);
|
|
186
|
+
groupMap.get(g).push(type);
|
|
187
|
+
}
|
|
188
|
+
const sortedGroups = Array.from(groupMap.keys()).sort();
|
|
189
|
+
for (const group of sortedGroups) {
|
|
190
|
+
const groupTypes = groupMap.get(group);
|
|
191
|
+
if (!group) {
|
|
192
|
+
// Root-level types (no group) — emit inline re-exports
|
|
193
|
+
for (const type of groupTypes) {
|
|
194
|
+
const exportKeyword = type.isProtocol ? "export type" : "export";
|
|
195
|
+
if (type.childTypes.length > 0) {
|
|
196
|
+
const exports = [type.typeName.name, ...type.childTypes.map((c) => c.typeName.name)];
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push(`${exportKeyword} {`);
|
|
199
|
+
for (const name of exports) {
|
|
200
|
+
lines.push(` ${name},`);
|
|
201
|
+
}
|
|
202
|
+
lines.push(`} from "./${toKebabCase(type.typeName.name)}";`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
lines.push(`${exportKeyword} { ${type.typeName.name} } from "./${toKebabCase(type.typeName.name)}";`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Group subfolder — re-export from the group's index.ts
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const type of groupTypes) {
|
|
213
|
+
const exportKeyword = type.isProtocol ? "export type" : "export";
|
|
214
|
+
const allNames = [type.typeName.name, ...type.childTypes.map(c => c.typeName.name)];
|
|
215
|
+
if (allNames.length > 1) {
|
|
216
|
+
lines.push(`${exportKeyword} { ${allNames.join(", ")} } from "./${group}/${toKebabCase(type.typeName.name)}";`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
lines.push(`${exportKeyword} { ${type.typeName.name} } from "./${group}/${toKebabCase(type.typeName.name)}";`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
lines.push("");
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Emit a group-level index.ts barrel file.
|
|
229
|
+
* Lives at `model/{group}/index.ts` and re-exports all types in that group.
|
|
230
|
+
*/
|
|
231
|
+
export function emitTypeScriptGroupIndex(group, groupNodes) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
lines.push("// Copyright (c) Microsoft. All rights reserved.");
|
|
234
|
+
lines.push("// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY.");
|
|
235
|
+
lines.push("");
|
|
236
|
+
for (const type of groupNodes) {
|
|
237
|
+
const exportKeyword = type.isProtocol ? "export type" : "export";
|
|
238
|
+
const allNames = [type.typeName.name, ...type.childTypes.map(c => c.typeName.name)];
|
|
239
|
+
if (allNames.length > 1) {
|
|
240
|
+
lines.push(`${exportKeyword} {`);
|
|
241
|
+
for (const name of allNames) {
|
|
242
|
+
lines.push(` ${name},`);
|
|
243
|
+
}
|
|
244
|
+
lines.push(`} from "./${toKebabCase(type.typeName.name)}";`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
lines.push(`${exportKeyword} { ${type.typeName.name} } from "./${toKebabCase(type.typeName.name)}";`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
lines.push("");
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// ESLint configuration
|
|
255
|
+
// ============================================================================
|
|
256
|
+
/**
|
|
257
|
+
* Emit the static ESLint configuration file.
|
|
258
|
+
*/
|
|
259
|
+
export function emitEslintConfig() {
|
|
260
|
+
return `// ESLint configuration for auto-generated Typra TypeScript code
|
|
261
|
+
// This file is auto-generated by the Typra emitter
|
|
262
|
+
import js from "@eslint/js";
|
|
263
|
+
import tseslint from "typescript-eslint";
|
|
264
|
+
|
|
265
|
+
export default tseslint.config(
|
|
266
|
+
js.configs.recommended,
|
|
267
|
+
...tseslint.configs.recommended,
|
|
268
|
+
{
|
|
269
|
+
files: ["src/**/*.ts"],
|
|
270
|
+
rules: {
|
|
271
|
+
// Allow unused vars prefixed with underscore
|
|
272
|
+
"@typescript-eslint/no-unused-vars": [
|
|
273
|
+
"error",
|
|
274
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
275
|
+
],
|
|
276
|
+
// Allow explicit any in generated code
|
|
277
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
278
|
+
// Allow non-null assertions in generated code
|
|
279
|
+
"@typescript-eslint/no-non-null-assertion": "off",
|
|
280
|
+
// Allow require() for dynamic YAML imports in generated code
|
|
281
|
+
"@typescript-eslint/no-require-imports": "off",
|
|
282
|
+
// Allow empty blocks in generated shorthand parsing patterns
|
|
283
|
+
"no-empty": "off",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
files: ["tests/**/*.ts"],
|
|
288
|
+
rules: {
|
|
289
|
+
// Allow explicit any in test code
|
|
290
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
291
|
+
// Allow unused vars in generated tests
|
|
292
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
293
|
+
// Allow require() for dynamic YAML imports
|
|
294
|
+
"@typescript-eslint/no-require-imports": "off",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
ignores: ["dist/**", "node_modules/**", "*.config.*"],
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=scaffolding.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript test file emitter — BaseTestContext → vitest source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the Nunjucks templates:
|
|
5
|
+
* - `test.ts.njk` → emitTypeScriptTest()
|
|
6
|
+
* - `_macros.njk` → factoryParamTestValue() (test-specific macro)
|
|
7
|
+
*
|
|
8
|
+
* The emitter produces vitest `describe`/`it` blocks for:
|
|
9
|
+
* - Construction (default + partial)
|
|
10
|
+
* - JSON serialization (load + round-trip per example)
|
|
11
|
+
* - YAML serialization (load + round-trip per example)
|
|
12
|
+
* - Alternate representations (scalar coercions)
|
|
13
|
+
* - Factory methods
|
|
14
|
+
* - Load/save (dictionary round-trip)
|
|
15
|
+
*/
|
|
16
|
+
import { BaseTestContext } from "../../ir/ast.js";
|
|
17
|
+
/**
|
|
18
|
+
* Emit a vitest test file for a TypeNode.
|
|
19
|
+
*/
|
|
20
|
+
export declare function emitTypeScriptTest(ctx: BaseTestContext & {
|
|
21
|
+
importPath: string;
|
|
22
|
+
namespace: string;
|
|
23
|
+
}): string;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript test file emitter — BaseTestContext → vitest source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the Nunjucks templates:
|
|
5
|
+
* - `test.ts.njk` → emitTypeScriptTest()
|
|
6
|
+
* - `_macros.njk` → factoryParamTestValue() (test-specific macro)
|
|
7
|
+
*
|
|
8
|
+
* The emitter produces vitest `describe`/`it` blocks for:
|
|
9
|
+
* - Construction (default + partial)
|
|
10
|
+
* - JSON serialization (load + round-trip per example)
|
|
11
|
+
* - YAML serialization (load + round-trip per example)
|
|
12
|
+
* - Alternate representations (scalar coercions)
|
|
13
|
+
* - Factory methods
|
|
14
|
+
* - Load/save (dictionary round-trip)
|
|
15
|
+
*/
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Macro replacements
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Map a factory parameter type string to a TypeScript test value literal.
|
|
21
|
+
* Replaces the `factoryParamTestValue` macro from `_macros.njk`.
|
|
22
|
+
*/
|
|
23
|
+
function factoryParamTestValue(typeStr) {
|
|
24
|
+
switch (typeStr) {
|
|
25
|
+
case "string":
|
|
26
|
+
return '"test"';
|
|
27
|
+
case "boolean":
|
|
28
|
+
return "true";
|
|
29
|
+
case "integer":
|
|
30
|
+
case "int32":
|
|
31
|
+
case "int64":
|
|
32
|
+
return "42";
|
|
33
|
+
case "float":
|
|
34
|
+
case "float64":
|
|
35
|
+
case "float32":
|
|
36
|
+
return "3.14";
|
|
37
|
+
case "unknown":
|
|
38
|
+
default:
|
|
39
|
+
return '"test"';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Render name in PascalCase (used by coercion test validation keys).
|
|
44
|
+
*/
|
|
45
|
+
function renderName(name) {
|
|
46
|
+
const pascal = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
47
|
+
return pascal.charAt(0).toUpperCase() + pascal.slice(1);
|
|
48
|
+
}
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Main entry point
|
|
51
|
+
// ============================================================================
|
|
52
|
+
/**
|
|
53
|
+
* Emit a vitest test file for a TypeNode.
|
|
54
|
+
*/
|
|
55
|
+
export function emitTypeScriptTest(ctx) {
|
|
56
|
+
const { node, isAbstract, examples, coercions, factories, importPath } = ctx;
|
|
57
|
+
const typeName = node.typeName.name;
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push("// Copyright (c) Microsoft. All rights reserved.");
|
|
60
|
+
lines.push("// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY.");
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(`import { ${typeName} } from "${importPath}";`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(`describe("${typeName}", () => {`);
|
|
65
|
+
lines.push(` describe("construction", () => {`);
|
|
66
|
+
lines.push(` it("should create a new instance with defaults", () => {`);
|
|
67
|
+
lines.push(` const instance = new ${typeName}();`);
|
|
68
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
69
|
+
lines.push(` });`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(` it("should create a new instance with partial initialization", () => {`);
|
|
72
|
+
lines.push(` const instance = new ${typeName}({});`);
|
|
73
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
74
|
+
lines.push(` });`);
|
|
75
|
+
lines.push(` });`);
|
|
76
|
+
// JSON serialization
|
|
77
|
+
if (examples.length > 0) {
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(` describe("JSON serialization", () => {`);
|
|
80
|
+
for (let i = 0; i < examples.length; i++) {
|
|
81
|
+
const example = examples[i];
|
|
82
|
+
const exampleNum = i + 1;
|
|
83
|
+
lines.push(` it("should load from JSON - example ${exampleNum}", () => {`);
|
|
84
|
+
lines.push(" const json = `" + example.json.join("\\n") + "`;");
|
|
85
|
+
lines.push(` const instance = ${typeName}.fromJson(json);`);
|
|
86
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
87
|
+
for (const val of example.validations) {
|
|
88
|
+
lines.push(` expect(instance.${val.key}).toEqual(${val.delimiter}${val.value}${val.delimiter});`);
|
|
89
|
+
}
|
|
90
|
+
lines.push(` });`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push(` it("should round-trip JSON - example ${exampleNum}", () => {`);
|
|
93
|
+
lines.push(" const json = `" + example.json.join("\\n") + "`;");
|
|
94
|
+
lines.push(` const instance = ${typeName}.fromJson(json);`);
|
|
95
|
+
lines.push(` const output = instance.toJson();`);
|
|
96
|
+
lines.push(` const reloaded = ${typeName}.fromJson(output);`);
|
|
97
|
+
for (const val of example.validations) {
|
|
98
|
+
lines.push(` expect(reloaded.${val.key}).toEqual(instance.${val.key});`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(` });`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(` });`);
|
|
103
|
+
// YAML serialization
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(` describe("YAML serialization", () => {`);
|
|
106
|
+
for (let i = 0; i < examples.length; i++) {
|
|
107
|
+
const example = examples[i];
|
|
108
|
+
const exampleNum = i + 1;
|
|
109
|
+
lines.push(` it("should load from YAML - example ${exampleNum}", () => {`);
|
|
110
|
+
lines.push(" const yaml = `" + example.yaml.join("\\n") + "`;");
|
|
111
|
+
lines.push(` const instance = ${typeName}.fromYaml(yaml);`);
|
|
112
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
113
|
+
for (const val of example.validations) {
|
|
114
|
+
lines.push(` expect(instance.${val.key}).toEqual(${val.delimiter}${val.value}${val.delimiter});`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(` });`);
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(` it("should round-trip YAML - example ${exampleNum}", () => {`);
|
|
119
|
+
lines.push(" const yaml = `" + example.yaml.join("\\n") + "`;");
|
|
120
|
+
lines.push(` const instance = ${typeName}.fromYaml(yaml);`);
|
|
121
|
+
lines.push(` const output = instance.toYaml();`);
|
|
122
|
+
lines.push(` const reloaded = ${typeName}.fromYaml(output);`);
|
|
123
|
+
for (const val of example.validations) {
|
|
124
|
+
lines.push(` expect(reloaded.${val.key}).toEqual(instance.${val.key});`);
|
|
125
|
+
}
|
|
126
|
+
lines.push(` });`);
|
|
127
|
+
}
|
|
128
|
+
lines.push(` });`);
|
|
129
|
+
}
|
|
130
|
+
// Alternate representations (coercions)
|
|
131
|
+
if (coercions.length > 0) {
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push(` describe("alternate representations", () => {`);
|
|
134
|
+
for (const alt of coercions) {
|
|
135
|
+
lines.push(` it("should handle ${alt.title} alternate representation", () => {`);
|
|
136
|
+
lines.push(` const value = ${alt.value};`);
|
|
137
|
+
lines.push(` const json = JSON.stringify(value);`);
|
|
138
|
+
lines.push(` const instance = ${typeName}.fromJson(json);`);
|
|
139
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
140
|
+
for (const val of alt.validations) {
|
|
141
|
+
const coercionKey = renderName(val.key).toLowerCase().replace(/\./g, "");
|
|
142
|
+
lines.push(` expect(instance.${coercionKey}).toEqual(${val.delimiter}${val.value}${val.delimiter});`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(` });`);
|
|
145
|
+
}
|
|
146
|
+
lines.push(` });`);
|
|
147
|
+
}
|
|
148
|
+
// Factory methods
|
|
149
|
+
if (factories.length > 0) {
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(` describe("factory methods", () => {`);
|
|
152
|
+
for (const factory of factories) {
|
|
153
|
+
const paramValues = Object.values(factory.params)
|
|
154
|
+
.map((pType) => factoryParamTestValue(pType))
|
|
155
|
+
.join(", ");
|
|
156
|
+
lines.push(` it("should create instance via ${factory.name}() factory", () => {`);
|
|
157
|
+
lines.push(` const instance = ${typeName}.${factory.name}(${paramValues});`);
|
|
158
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
159
|
+
lines.push(` expect(instance).toBeInstanceOf(${typeName});`);
|
|
160
|
+
for (const [propName, value] of Object.entries(factory.sets)) {
|
|
161
|
+
if (value === true) {
|
|
162
|
+
lines.push(` expect(instance.${propName}).toBe(true);`);
|
|
163
|
+
}
|
|
164
|
+
else if (value === false) {
|
|
165
|
+
lines.push(` expect(instance.${propName}).toBe(false);`);
|
|
166
|
+
}
|
|
167
|
+
else if (typeof value === "number") {
|
|
168
|
+
lines.push(` expect(instance.${propName}).toBe(${value});`);
|
|
169
|
+
}
|
|
170
|
+
else if (typeof value === "string") {
|
|
171
|
+
lines.push(` expect(instance.${propName}).toBe("${value}");`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
lines.push(` });`);
|
|
175
|
+
}
|
|
176
|
+
lines.push(` });`);
|
|
177
|
+
}
|
|
178
|
+
// Load and save
|
|
179
|
+
lines.push("");
|
|
180
|
+
if (!(isAbstract && node.isAbstract)) {
|
|
181
|
+
lines.push(` describe("load and save", () => {`);
|
|
182
|
+
if (!isAbstract) {
|
|
183
|
+
lines.push(` it("should load from dictionary", () => {`);
|
|
184
|
+
lines.push(` const data: Record<string, unknown> = {};`);
|
|
185
|
+
lines.push(` const instance = ${typeName}.load(data);`);
|
|
186
|
+
lines.push(` expect(instance).toBeDefined();`);
|
|
187
|
+
lines.push(` });`);
|
|
188
|
+
}
|
|
189
|
+
lines.push("");
|
|
190
|
+
if (!node.isAbstract) {
|
|
191
|
+
lines.push(` it("should save to dictionary", () => {`);
|
|
192
|
+
lines.push(` const instance = new ${typeName}();`);
|
|
193
|
+
lines.push(` const data = instance.save();`);
|
|
194
|
+
lines.push(` expect(data).toBeDefined();`);
|
|
195
|
+
lines.push(` expect(typeof data).toBe("object");`);
|
|
196
|
+
lines.push(` });`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(` });`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("});");
|
|
201
|
+
lines.push("");
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=test-emitter.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript expression visitor — Expr IR → TypeScript source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { Expr, TypeRegistry } from "../../ir/expansion.js";
|
|
5
|
+
import { ExprVisitor } from "../../ir/visitor.js";
|
|
6
|
+
export declare class TypeScriptExprVisitor implements ExprVisitor {
|
|
7
|
+
registry?: TypeRegistry;
|
|
8
|
+
constructor(registry?: TypeRegistry);
|
|
9
|
+
visitExpr(expr: Expr): string;
|
|
10
|
+
private visitConstruct;
|
|
11
|
+
private visitVariant;
|
|
12
|
+
private visitArray;
|
|
13
|
+
private escapeString;
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript expression visitor — Expr IR → TypeScript source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { assertNever } from "../../ir/visitor.js";
|
|
5
|
+
export class TypeScriptExprVisitor {
|
|
6
|
+
registry;
|
|
7
|
+
constructor(registry) {
|
|
8
|
+
this.registry = registry;
|
|
9
|
+
}
|
|
10
|
+
visitExpr(expr) {
|
|
11
|
+
switch (expr.kind) {
|
|
12
|
+
case "string":
|
|
13
|
+
return `"${this.escapeString(expr.value)}"`;
|
|
14
|
+
case "number":
|
|
15
|
+
return String(expr.value);
|
|
16
|
+
case "boolean":
|
|
17
|
+
return expr.value ? "true" : "false";
|
|
18
|
+
case "null":
|
|
19
|
+
return "undefined";
|
|
20
|
+
case "param":
|
|
21
|
+
return expr.name;
|
|
22
|
+
case "construct":
|
|
23
|
+
return this.visitConstruct(expr);
|
|
24
|
+
case "variant":
|
|
25
|
+
return this.visitVariant(expr);
|
|
26
|
+
case "array":
|
|
27
|
+
return this.visitArray(expr);
|
|
28
|
+
case "dict":
|
|
29
|
+
return `{ ${expr.entries.map(e => `${e.key}: ${this.visitExpr(e.value)}`).join(", ")} }`;
|
|
30
|
+
case "field_read":
|
|
31
|
+
return `${expr.objectName}.${expr.fieldName}`;
|
|
32
|
+
default:
|
|
33
|
+
return assertNever(expr);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
visitConstruct(expr) {
|
|
37
|
+
const typeName = expr.typeName.name;
|
|
38
|
+
if (expr.fields.length === 0) {
|
|
39
|
+
return `new ${typeName}({})`;
|
|
40
|
+
}
|
|
41
|
+
const fields = expr.fields.map(f => `${f.propertyName}: ${this.visitExpr(f.value)}`).join(", ");
|
|
42
|
+
return `new ${typeName}({ ${fields} })`;
|
|
43
|
+
}
|
|
44
|
+
visitVariant(expr) {
|
|
45
|
+
// In TypeScript, polymorphic children are full classes — construct the variant type directly
|
|
46
|
+
const variantName = expr.variantTypeName.name;
|
|
47
|
+
if (expr.fields.length === 0) {
|
|
48
|
+
return `new ${variantName}({})`;
|
|
49
|
+
}
|
|
50
|
+
const fields = expr.fields.map(f => `${f.propertyName}: ${this.visitExpr(f.value)}`).join(", ");
|
|
51
|
+
return `new ${variantName}({ ${fields} })`;
|
|
52
|
+
}
|
|
53
|
+
visitArray(expr) {
|
|
54
|
+
if (expr.items.length === 0) {
|
|
55
|
+
return "[]";
|
|
56
|
+
}
|
|
57
|
+
const items = expr.items.map(i => this.visitExpr(i)).join(", ");
|
|
58
|
+
return `[${items}]`;
|
|
59
|
+
}
|
|
60
|
+
escapeString(s) {
|
|
61
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=visitor.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface EmitTarget {
|
|
2
|
+
"type": string;
|
|
3
|
+
"output-dir"?: string;
|
|
4
|
+
"test-dir"?: string;
|
|
5
|
+
"alias"?: {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
};
|
|
8
|
+
"format"?: boolean;
|
|
9
|
+
"namespace"?: string;
|
|
10
|
+
"import-path"?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TypraEmitterOptions {
|
|
13
|
+
"root-object": string;
|
|
14
|
+
"emit-targets"?: EmitTarget[];
|
|
15
|
+
"root-namespace"?: string;
|
|
16
|
+
"root-alias"?: string;
|
|
17
|
+
"omit-models"?: string[];
|
|
18
|
+
"schema-output-dir"?: string;
|
|
19
|
+
"additional-roots"?: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
22
|
+
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
|
23
|
+
}, TypraEmitterOptions, "samples" | "coercions" | "abstracts" | "factories" | "methods" | "knownAs" | "defaultFor" | "protocols">;
|
|
24
|
+
export declare const reportDiagnostic: <C extends string | number, M extends keyof {
|
|
25
|
+
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
|
26
|
+
}[C]>(program: import("@typespec/compiler").Program, diag: import("@typespec/compiler").DiagnosticReport<{
|
|
27
|
+
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
|
28
|
+
}, C, M>) => void, createDiagnostic: <C extends string | number, M extends keyof {
|
|
29
|
+
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
|
30
|
+
}[C]>(diag: import("@typespec/compiler").DiagnosticReport<{
|
|
31
|
+
[code: string]: import("@typespec/compiler").DiagnosticMessages;
|
|
32
|
+
}, C, M>) => import("@typespec/compiler").Diagnostic;
|
|
33
|
+
export declare const StateKeys: Record<"samples" | "coercions" | "abstracts" | "factories" | "methods" | "knownAs" | "defaultFor" | "protocols", symbol>;
|