@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.
Files changed (82) hide show
  1. package/dist/src/cleanup/generated-file.d.ts +6 -0
  2. package/dist/src/cleanup/generated-file.js +61 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +110 -0
  5. package/dist/src/decorators.d.ts +56 -0
  6. package/dist/src/decorators.js +177 -0
  7. package/dist/src/emitter.d.ts +13 -0
  8. package/dist/src/emitter.js +137 -0
  9. package/dist/src/generate.d.ts +86 -0
  10. package/dist/src/generate.js +104 -0
  11. package/dist/src/index.d.ts +4 -0
  12. package/dist/src/index.js +5 -0
  13. package/dist/src/ir/ast.d.ts +235 -0
  14. package/dist/src/ir/ast.js +589 -0
  15. package/dist/src/ir/declarations.d.ts +364 -0
  16. package/dist/src/ir/declarations.js +23 -0
  17. package/dist/src/ir/expansion.d.ts +140 -0
  18. package/dist/src/ir/expansion.js +407 -0
  19. package/dist/src/ir/lower.d.ts +53 -0
  20. package/dist/src/ir/lower.js +480 -0
  21. package/dist/src/ir/utilities.d.ts +12 -0
  22. package/dist/src/ir/utilities.js +39 -0
  23. package/dist/src/ir/visitor.d.ts +29 -0
  24. package/dist/src/ir/visitor.js +48 -0
  25. package/dist/src/languages/csharp/driver.d.ts +5 -0
  26. package/dist/src/languages/csharp/driver.js +315 -0
  27. package/dist/src/languages/csharp/emitter.d.ts +33 -0
  28. package/dist/src/languages/csharp/emitter.js +1140 -0
  29. package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
  30. package/dist/src/languages/csharp/scaffolding.js +591 -0
  31. package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
  32. package/dist/src/languages/csharp/test-emitter.js +274 -0
  33. package/dist/src/languages/csharp/visitor.d.ts +14 -0
  34. package/dist/src/languages/csharp/visitor.js +79 -0
  35. package/dist/src/languages/go/driver.d.ts +12 -0
  36. package/dist/src/languages/go/driver.js +128 -0
  37. package/dist/src/languages/go/emitter.d.ts +33 -0
  38. package/dist/src/languages/go/emitter.js +879 -0
  39. package/dist/src/languages/go/scaffolding.d.ts +18 -0
  40. package/dist/src/languages/go/scaffolding.js +53 -0
  41. package/dist/src/languages/go/test-emitter.d.ts +20 -0
  42. package/dist/src/languages/go/test-emitter.js +300 -0
  43. package/dist/src/languages/go/visitor.d.ts +14 -0
  44. package/dist/src/languages/go/visitor.js +78 -0
  45. package/dist/src/languages/markdown/driver.d.ts +19 -0
  46. package/dist/src/languages/markdown/driver.js +408 -0
  47. package/dist/src/languages/python/driver.d.ts +14 -0
  48. package/dist/src/languages/python/driver.js +372 -0
  49. package/dist/src/languages/python/emitter.d.ts +31 -0
  50. package/dist/src/languages/python/emitter.js +856 -0
  51. package/dist/src/languages/python/scaffolding.d.ts +33 -0
  52. package/dist/src/languages/python/scaffolding.js +279 -0
  53. package/dist/src/languages/python/test-emitter.d.ts +29 -0
  54. package/dist/src/languages/python/test-emitter.js +388 -0
  55. package/dist/src/languages/python/visitor.d.ts +14 -0
  56. package/dist/src/languages/python/visitor.js +65 -0
  57. package/dist/src/languages/rust/driver.d.ts +13 -0
  58. package/dist/src/languages/rust/driver.js +624 -0
  59. package/dist/src/languages/rust/emitter.d.ts +45 -0
  60. package/dist/src/languages/rust/emitter.js +1596 -0
  61. package/dist/src/languages/rust/visitor.d.ts +25 -0
  62. package/dist/src/languages/rust/visitor.js +153 -0
  63. package/dist/src/languages/typescript/driver.d.ts +8 -0
  64. package/dist/src/languages/typescript/driver.js +209 -0
  65. package/dist/src/languages/typescript/emitter.d.ts +42 -0
  66. package/dist/src/languages/typescript/emitter.js +904 -0
  67. package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
  68. package/dist/src/languages/typescript/scaffolding.js +303 -0
  69. package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
  70. package/dist/src/languages/typescript/test-emitter.js +204 -0
  71. package/dist/src/languages/typescript/visitor.d.ts +14 -0
  72. package/dist/src/languages/typescript/visitor.js +64 -0
  73. package/dist/src/lib.d.ts +33 -0
  74. package/dist/src/lib.js +101 -0
  75. package/dist/src/testing/index.d.ts +2 -0
  76. package/dist/src/testing/index.js +8 -0
  77. package/dist/src/testing/test-context.d.ts +63 -0
  78. package/dist/src/testing/test-context.js +355 -0
  79. package/fixtures/shapes/main.tsp +43 -0
  80. package/fixtures/tspconfig.yaml +13 -0
  81. package/package.json +76 -0
  82. package/src/lib/main.tsp +110 -0
@@ -0,0 +1,315 @@
1
+ import { resolvePath } from "@typespec/compiler";
2
+ import { enumerateTypes } from "../../ir/ast.js";
3
+ import { filterNodes } from "../../emitter.js";
4
+ import { getCombinations, scalarValue } from "../../ir/utilities.js";
5
+ import * as YAML from "yaml";
6
+ import { resolve, dirname } from "path";
7
+ import { execFileSync } from "child_process";
8
+ import { existsSync, readdirSync } from "fs";
9
+ import { TypeRegistry } from "../../ir/expansion.js";
10
+ import { CSharpExprVisitor } from "./visitor.js";
11
+ import { lowerType, collectPolymorphicTypeNames } from "../../ir/lower.js";
12
+ import { emitCSharpClass, emitCSharpEnum } from "./emitter.js";
13
+ import { emitCSharpContext, emitCSharpUtils } from "./scaffolding.js";
14
+ import { emitCSharpTest } from "./test-emitter.js";
15
+ import { toPascalCase } from "../../ir/visitor.js";
16
+ import { emitGeneratedFile } from "../../cleanup/generated-file.js";
17
+ /**
18
+ * Stale-file deletion is intentionally disabled until manifest cleanup is enabled.
19
+ */
20
+ function cleanupFlatTypeFiles(relDir, isTypeFile) {
21
+ void relDir;
22
+ void isTypeFile;
23
+ return;
24
+ }
25
+ function cleanupGeneratedCSharpFiles(relDir) {
26
+ void relDir;
27
+ return;
28
+ }
29
+ export const generateCsharp = async (context, node, emitTarget, options) => {
30
+ const allTypes = Array.from(enumerateTypes(node));
31
+ const nodes = filterNodes(allTypes, options);
32
+ cleanupGeneratedCSharpFiles(emitTarget["output-dir"]);
33
+ cleanupGeneratedCSharpFiles(emitTarget["test-dir"]);
34
+ // Stale flat-file cleanup is disabled in this slice.
35
+ const isCsTypeFile = (name) => name.endsWith(".cs") && name !== "Context.cs" && name !== "Utils.cs" &&
36
+ !name.endsWith("Helpers.cs") && !name.endsWith("Extensions.cs");
37
+ cleanupFlatTypeFiles(emitTarget["output-dir"], isCsTypeFile);
38
+ cleanupFlatTypeFiles(emitTarget["test-dir"], isCsTypeFile);
39
+ // Build the expression IR infrastructure
40
+ const registry = TypeRegistry.fromTypeGraph(allTypes);
41
+ const visitor = new CSharpExprVisitor(registry);
42
+ // Determine namespace: use explicit override from config, or fall back to TypeSpec namespace
43
+ const originalNamespace = node.typeName.namespace;
44
+ const csharpNamespace = emitTarget.namespace ?? originalNamespace;
45
+ // Emit context classes (LoadContext, SaveContext)
46
+ const contextCode = emitCSharpContext(csharpNamespace);
47
+ await emitCsharpFile(context, node, contextCode, "Context.cs", emitTarget["output-dir"]);
48
+ const utils = emitCSharpUtils(csharpNamespace);
49
+ await emitCsharpFile(context, node, utils, "Utils.cs", emitTarget["output-dir"]);
50
+ // Build Declaration IR once (loop-invariant)
51
+ const polyNames = collectPolymorphicTypeNames(allTypes[0], registry);
52
+ const allTypeDecls = nodes.map(nd => lowerType(nd, registry, polyNames));
53
+ const findTypeDecl = (name) => allTypeDecls.find(t => t.typeName.name === name);
54
+ // Collect and emit unique enum types from all fields
55
+ // Map each enum to the group of the first type that uses it
56
+ const emittedEnums = new Set();
57
+ const enumGroup = new Map(); // enumName → group
58
+ for (let i = 0; i < allTypeDecls.length; i++) {
59
+ const typeDecl = allTypeDecls[i];
60
+ const nodeGroup = nodes[i]?.group || "";
61
+ for (const field of typeDecl.fields) {
62
+ if (field.enumName && !emittedEnums.has(field.enumName)) {
63
+ enumGroup.set(field.enumName, nodeGroup);
64
+ }
65
+ }
66
+ }
67
+ for (const typeDecl of allTypeDecls) {
68
+ for (const field of typeDecl.fields) {
69
+ if (field.enumName && !field.isOpenEnum && field.allowedValues.length > 0 && !emittedEnums.has(field.enumName)) {
70
+ emittedEnums.add(field.enumName);
71
+ const enumCode = emitCSharpEnum({ name: field.enumName, values: field.allowedValues, isOpen: field.isOpenEnum }, csharpNamespace);
72
+ const csEnumName = field.enumName.charAt(0).toUpperCase() + field.enumName.slice(1);
73
+ const grp = enumGroup.get(field.enumName) || "";
74
+ const enumOutDir = grp ? `${emitTarget["output-dir"]}/${grp}` : emitTarget["output-dir"];
75
+ await emitCsharpFile(context, nodes[0], enumCode, `${csEnumName}.cs`, enumOutDir);
76
+ }
77
+ }
78
+ }
79
+ for (const n of nodes) {
80
+ const typeDecl = lowerType(n, registry, polyNames);
81
+ const classCode = emitCSharpClass(typeDecl, csharpNamespace, visitor, allTypeDecls, findTypeDecl);
82
+ // Emit into group subfolder (C# uses namespaces, no re-export files needed)
83
+ const outDir = n.group ? `${emitTarget["output-dir"]}/${n.group}` : emitTarget["output-dir"];
84
+ await emitCsharpFile(context, n, classCode, `${n.typeName.name}.cs`, outDir);
85
+ if (emitTarget["test-dir"] && !n.isProtocol) {
86
+ const testDir = n.group ? `${emitTarget["test-dir"]}/${n.group}` : emitTarget["test-dir"];
87
+ await emitCsharpFile(context, n, renderTests(n, csharpNamespace), `${n.typeName.name}ConversionTests.cs`, testDir);
88
+ }
89
+ }
90
+ // Format emitted files if format option is enabled (default: true)
91
+ if (emitTarget.format !== false) {
92
+ const outputDir = emitTarget["output-dir"]
93
+ ? resolve(process.cwd(), emitTarget["output-dir"])
94
+ : context.emitterOutputDir;
95
+ const testDir = emitTarget["test-dir"]
96
+ ? resolve(process.cwd(), emitTarget["test-dir"])
97
+ : undefined;
98
+ formatCSharpFiles(outputDir, testDir);
99
+ }
100
+ };
101
+ // --- Test-rendering helpers ---
102
+ const renderTests = (node, namespace) => {
103
+ const samples = node.properties.filter(p => p.samples && p.samples.length > 0).map(p => {
104
+ return p.samples?.map(s => ({
105
+ ...s.sample,
106
+ }));
107
+ });
108
+ const combinations = samples.length > 0 ?
109
+ getCombinations(samples) :
110
+ [];
111
+ const examples = combinations.map(c => {
112
+ const sample = Object.assign({}, ...c);
113
+ // Create YAML document and customize string scalar style for values with special chars
114
+ const doc = new YAML.Document(sample);
115
+ YAML.visit(doc, {
116
+ Scalar(key, node) {
117
+ // Only quote string values that contain special characters requiring escaping
118
+ if (typeof node.value === 'string') {
119
+ const str = node.value;
120
+ if (str.includes('\n') || str.includes('\t') || str.includes('#') || str.includes(':') || str.includes('"')) {
121
+ node.type = 'QUOTE_DOUBLE';
122
+ }
123
+ }
124
+ }
125
+ });
126
+ return {
127
+ json: JSON.stringify(sample, null, 2).split('\n'),
128
+ yaml: doc.toString({ indent: 2, lineWidth: 0 }).split('\n'),
129
+ // get all scalars in the sample - using 'validations' (plural) for consistency across languages
130
+ validations: Object.keys(sample).filter(key => typeof sample[key] !== 'object').map(key => {
131
+ const val = sample[key];
132
+ // Check if this field is a closed enum — if so, use EnumName.MemberName syntax
133
+ // Skip discriminator fields — their enums are excluded from generation
134
+ const prop = node.properties.find(p => p.name === key);
135
+ const isDiscriminator = node.discriminator === key;
136
+ if (prop && prop.enumName && !prop.isOpenEnum && !isDiscriminator && typeof val === 'string') {
137
+ const csEnumName = toPascalCase(prop.enumName);
138
+ const memberName = toPascalCase(val);
139
+ return {
140
+ key: renderName(key),
141
+ value: `${csEnumName}.${memberName}`,
142
+ startDelim: '',
143
+ endDelim: '',
144
+ };
145
+ }
146
+ const needsVerbatim = typeof val === 'string' && (val.includes('\n') || val.includes('"'));
147
+ return {
148
+ key: renderName(key),
149
+ value: typeof val === 'boolean' ? (val ? "True" : "False") :
150
+ (needsVerbatim ? val.replace(/"/g, '""') : val),
151
+ startDelim: typeof val === 'string' ? (needsVerbatim ? '@"' : '"') : '',
152
+ endDelim: typeof val === 'string' ? '"' :
153
+ typeof val === 'number' && !Number.isInteger(val) ? 'f' : '',
154
+ };
155
+ }),
156
+ };
157
+ });
158
+ const coercions = node.coercions.map(alt => {
159
+ const example = alt.example ? (typeof (alt.example) === "string" ? '"' + alt.example + '"' : alt.example.toString()) : scalarValue[alt.scalar] || "None";
160
+ return {
161
+ title: alt.title || alt.scalar,
162
+ scalar: alt.scalar,
163
+ value: example,
164
+ // using 'validations' (plural) for consistency across languages
165
+ validations: Object.keys(alt.expansion).filter(key => typeof alt.expansion[key] !== 'object').map(key => {
166
+ const value = alt.expansion[key] === "{value}" ? example : alt.expansion[key];
167
+ // Check if this field is a closed enum (skip discriminator fields)
168
+ const prop = node.properties.find(p => p.name === key);
169
+ const isDiscriminator = node.discriminator === key;
170
+ if (prop && prop.enumName && !prop.isOpenEnum && !isDiscriminator) {
171
+ // Extract the raw string value (strip quotes if present from example substitution)
172
+ const rawValue = typeof value === 'string' ? value.replace(/^"|"$/g, '') : String(value);
173
+ const csEnumName = toPascalCase(prop.enumName);
174
+ const memberName = toPascalCase(rawValue);
175
+ return {
176
+ key: renderName(key),
177
+ value: `${csEnumName}.${memberName}`,
178
+ delimiter: '',
179
+ };
180
+ }
181
+ return {
182
+ key: renderName(key),
183
+ value: value,
184
+ delimiter: typeof value === 'string' && !value.includes('"') && alt.expansion[key] !== "{value}" ? '"' : '',
185
+ };
186
+ }),
187
+ };
188
+ });
189
+ return emitCSharpTest({
190
+ node,
191
+ namespace,
192
+ examples,
193
+ coercions,
194
+ factories: node.factories,
195
+ renderName,
196
+ renderCsharpFactoryMethodName: (factoryName) => renderCsharpFactoryMethodName(factoryName, node),
197
+ renderCsharpFactoryTestValue,
198
+ });
199
+ };
200
+ const renderName = (name) => {
201
+ // convert snake_case to PascalCase
202
+ const pascal = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
203
+ // capitalize the first letter
204
+ return pascal.charAt(0).toUpperCase() + pascal.slice(1);
205
+ };
206
+ const renderCsharpFactoryParamType = (typeStr) => {
207
+ switch (typeStr) {
208
+ case "string": return "string";
209
+ case "boolean": return "bool";
210
+ case "integer":
211
+ case "int32": return "int";
212
+ case "int64": return "long";
213
+ case "float":
214
+ case "float32": return "float";
215
+ case "float64": return "double";
216
+ case "unknown": return "object?";
217
+ default: return "object?";
218
+ }
219
+ };
220
+ // Returns a factory method name that won't clash with C# property names on the same type.
221
+ // If the capitalized factory name matches a property name, prefix with "Create".
222
+ const renderCsharpFactoryMethodName = (factoryName, node) => {
223
+ const methodName = factoryName.charAt(0).toUpperCase() + factoryName.slice(1);
224
+ const propertyNames = node.properties.map(p => renderName(p.name));
225
+ // Also consider zero-param non-verb method stubs that C# emits as properties
226
+ for (const m of node.methods) {
227
+ if (!m.params?.length) {
228
+ const mName = renderName(m.name);
229
+ if (!propertyNames.includes(mName)) {
230
+ propertyNames.push(mName);
231
+ }
232
+ }
233
+ }
234
+ if (propertyNames.includes(methodName)) {
235
+ return `Create${methodName}`;
236
+ }
237
+ return methodName;
238
+ };
239
+ const renderCsharpFactoryTestValue = (typeStr) => {
240
+ switch (typeStr) {
241
+ case "string": return '"test"';
242
+ case "boolean": return "true";
243
+ case "integer":
244
+ case "int32": return "42";
245
+ case "int64": return "42L";
246
+ case "float":
247
+ case "float32": return "3.14f";
248
+ case "float64": return "3.14";
249
+ case "unknown": return '"test"';
250
+ default: return '"test"';
251
+ }
252
+ };
253
+ const emitCsharpFile = async (context, type, python, filename, outputDir) => {
254
+ outputDir = outputDir || `${context.emitterOutputDir}/CSharp`;
255
+ const typePath = type.typeName.namespace.split(".");
256
+ // replace typename with file
257
+ typePath.push(filename);
258
+ const path = resolvePath(outputDir, filename);
259
+ await emitGeneratedFile(context, path, python);
260
+ };
261
+ /**
262
+ * Format C# files using dotnet format.
263
+ * Runs formatter from the .NET project root (where .csproj or .sln is located).
264
+ */
265
+ function formatCSharpFiles(outputDir, testDir) {
266
+ const dirs = [outputDir, ...(testDir ? [testDir] : [])];
267
+ const formatted = new Set();
268
+ for (const dir of dirs) {
269
+ const projectRoot = findDotNetProjectRoot(dir);
270
+ if (!projectRoot) {
271
+ console.warn(`Warning: Could not find .csproj or .sln file for ${dir}. Skipping formatting.`);
272
+ continue;
273
+ }
274
+ // Avoid formatting the same project twice
275
+ if (formatted.has(projectRoot)) {
276
+ continue;
277
+ }
278
+ formatted.add(projectRoot);
279
+ try {
280
+ execFileSync("dotnet", ["format", projectRoot], {
281
+ cwd: dirname(projectRoot),
282
+ stdio: 'pipe',
283
+ encoding: 'utf-8'
284
+ });
285
+ }
286
+ catch (error) {
287
+ console.warn(`Warning: dotnet format failed for ${projectRoot}. You may need to run it manually.`);
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * Find the .NET project root by traversing up from the output directory
293
+ * looking for .csproj or .sln files.
294
+ */
295
+ function findDotNetProjectRoot(startDir) {
296
+ let currentDir = resolve(startDir);
297
+ const root = resolve('/');
298
+ // On Windows, also check for drive root (e.g., "C:\")
299
+ while (currentDir !== root && currentDir !== dirname(currentDir)) {
300
+ // First check for .csproj (more specific)
301
+ const files = existsSync(currentDir) ? readdirSync(currentDir) : [];
302
+ const csprojFile = files.find((f) => f.endsWith('.csproj'));
303
+ if (csprojFile) {
304
+ return resolve(currentDir, csprojFile);
305
+ }
306
+ // Then check for .sln
307
+ const slnFile = files.find((f) => f.endsWith('.sln'));
308
+ if (slnFile) {
309
+ return resolve(currentDir, slnFile);
310
+ }
311
+ currentDir = dirname(currentDir);
312
+ }
313
+ return undefined;
314
+ }
315
+ //# sourceMappingURL=driver.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * C# code emitter — Declaration IR → C# source code.
3
+ *
4
+ * Replaces `file.cs.njk` Nunjucks template with a typed TypeScript function
5
+ * that walks the TypeDecl tree and produces a complete C# class file.
6
+ *
7
+ * Each TypeDecl becomes one C# file (one class per file).
8
+ * Polymorphic types use abstract class + child : Base inheritance.
9
+ *
10
+ * Structural blocks emitted (in order):
11
+ * 1. Copyright + using directives
12
+ * 2. Namespace
13
+ * 3. XML doc comment
14
+ * 4. Class declaration
15
+ * 5. ShorthandProperty
16
+ * 6. Constructor
17
+ * 7. Properties
18
+ * 8. #region Load Methods
19
+ * 9. #region Save Methods
20
+ * 10. #region Factory Methods (if any)
21
+ * 11. #region Helpers (if any)
22
+ */
23
+ import { TypeDecl, EnumDef } from "../../ir/declarations.js";
24
+ import { ExprVisitor } from "../../ir/visitor.js";
25
+ /**
26
+ * Emit a complete C# class file for a single TypeDecl.
27
+ */
28
+ export declare function emitCSharpClass(type: TypeDecl, namespace: string, visitor: ExprVisitor, allTypes: TypeDecl[], findType: (name: string) => TypeDecl | undefined): string;
29
+ /**
30
+ * Emit a C# enum file for a named string-literal type.
31
+ * Uses JsonStringEnumConverter for JSON serialization.
32
+ */
33
+ export declare function emitCSharpEnum(enumDef: EnumDef, namespace: string): string;