@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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust expression visitor — Expr IR → Rust source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { Expr, TypeRegistry } from "../../ir/expansion.js";
|
|
5
|
+
import { ExprVisitor } from "../../ir/visitor.js";
|
|
6
|
+
export declare class RustExprVisitor implements ExprVisitor {
|
|
7
|
+
registry?: TypeRegistry;
|
|
8
|
+
constructor(registry?: TypeRegistry);
|
|
9
|
+
visitExpr(expr: Expr): string;
|
|
10
|
+
private visitConstruct;
|
|
11
|
+
/**
|
|
12
|
+
* Handle Construct on a polymorphic type — the discriminator field becomes an enum variant.
|
|
13
|
+
* E.g., Property { kind: "boolean", example: v } → Property { kind: PropertyKind::Custom { kind_name: "boolean".to_string() }, example: Some(v.into()), ..Default::default() }
|
|
14
|
+
*/
|
|
15
|
+
private visitPolymorphicConstruct;
|
|
16
|
+
private visitVariant;
|
|
17
|
+
private visitArray;
|
|
18
|
+
/**
|
|
19
|
+
* Wrap field values appropriately for Rust — Option<T> fields need Some(),
|
|
20
|
+
* string fields need .into(), etc.
|
|
21
|
+
*/
|
|
22
|
+
private wrapFieldValue;
|
|
23
|
+
private escapeString;
|
|
24
|
+
private visitFieldRead;
|
|
25
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust expression visitor — Expr IR → Rust source fragments.
|
|
3
|
+
*/
|
|
4
|
+
import { assertNever } from "../../ir/visitor.js";
|
|
5
|
+
import { toSnakeCase } from "../../ir/utilities.js";
|
|
6
|
+
const RUST_KEYWORDS = new Set([
|
|
7
|
+
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
|
8
|
+
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
|
9
|
+
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
|
10
|
+
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
|
|
11
|
+
"async", "await", "dyn",
|
|
12
|
+
]);
|
|
13
|
+
function rustFieldName(name) {
|
|
14
|
+
const snake = toSnakeCase(name);
|
|
15
|
+
return RUST_KEYWORDS.has(snake) ? `r#${snake}` : snake;
|
|
16
|
+
}
|
|
17
|
+
export class RustExprVisitor {
|
|
18
|
+
registry;
|
|
19
|
+
constructor(registry) {
|
|
20
|
+
this.registry = registry;
|
|
21
|
+
}
|
|
22
|
+
visitExpr(expr) {
|
|
23
|
+
switch (expr.kind) {
|
|
24
|
+
case "string":
|
|
25
|
+
return `"${this.escapeString(expr.value)}".to_string()`;
|
|
26
|
+
case "number":
|
|
27
|
+
return String(expr.value);
|
|
28
|
+
case "boolean":
|
|
29
|
+
return expr.value ? "true" : "false";
|
|
30
|
+
case "null":
|
|
31
|
+
return "None";
|
|
32
|
+
case "param":
|
|
33
|
+
// Always use .into() in Rust — handles String→String, bool→Value, i64→Value, etc.
|
|
34
|
+
return `${toSnakeCase(expr.name)}.into()`;
|
|
35
|
+
case "construct":
|
|
36
|
+
return this.visitConstruct(expr);
|
|
37
|
+
case "variant":
|
|
38
|
+
return this.visitVariant(expr);
|
|
39
|
+
case "array":
|
|
40
|
+
return this.visitArray(expr);
|
|
41
|
+
case "dict":
|
|
42
|
+
return `serde_json::json!({${expr.entries.map(e => `"${e.key}": ${this.visitExpr(e.value)}`).join(", ")}})`;
|
|
43
|
+
case "field_read":
|
|
44
|
+
return this.visitFieldRead(expr);
|
|
45
|
+
default:
|
|
46
|
+
return assertNever(expr);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
visitConstruct(expr) {
|
|
50
|
+
const typeName = expr.typeName.name;
|
|
51
|
+
// Check if this is a polymorphic type — discriminator fields need enum variant wrapping
|
|
52
|
+
const typeNode = this.registry?.get(typeName);
|
|
53
|
+
if (typeNode?.discriminator && typeNode.childTypes.length > 0) {
|
|
54
|
+
return this.visitPolymorphicConstruct(expr, typeNode);
|
|
55
|
+
}
|
|
56
|
+
if (expr.fields.length === 0) {
|
|
57
|
+
return `${typeName} { ..Default::default() }`;
|
|
58
|
+
}
|
|
59
|
+
const fields = expr.fields.map(f => {
|
|
60
|
+
let val = this.wrapFieldValue(f);
|
|
61
|
+
// For enum fields, convert string literals to EnumName::VariantName
|
|
62
|
+
if (f.value.kind === "string" && typeNode) {
|
|
63
|
+
const prop = typeNode.properties.find(p => p.name === f.propertyName);
|
|
64
|
+
if (prop?.enumName) {
|
|
65
|
+
const variantName = f.value.value.charAt(0).toUpperCase() + f.value.value.slice(1);
|
|
66
|
+
const enumVal = `${prop.enumName}::${variantName}`;
|
|
67
|
+
val = f.isOptional ? `Some(${enumVal})` : enumVal;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return `${toSnakeCase(f.propertyName)}: ${val}`;
|
|
71
|
+
}).join(", ");
|
|
72
|
+
return `${typeName} { ${fields}, ..Default::default() }`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Handle Construct on a polymorphic type — the discriminator field becomes an enum variant.
|
|
76
|
+
* E.g., Property { kind: "boolean", example: v } → Property { kind: PropertyKind::Custom { kind_name: "boolean".to_string() }, example: Some(v.into()), ..Default::default() }
|
|
77
|
+
*/
|
|
78
|
+
visitPolymorphicConstruct(expr, typeNode) {
|
|
79
|
+
const typeName = expr.typeName.name;
|
|
80
|
+
const enumName = `${typeName}Kind`;
|
|
81
|
+
const discFieldName = typeNode.discriminator;
|
|
82
|
+
// Find the discriminator field assignment
|
|
83
|
+
const discField = expr.fields.find(f => f.propertyName === discFieldName);
|
|
84
|
+
const discValue = discField?.value;
|
|
85
|
+
// If no discriminator field or not a string literal, fall back to normal construction
|
|
86
|
+
if (!discField || discValue?.kind !== "string") {
|
|
87
|
+
const fields = expr.fields.map(f => `${toSnakeCase(f.propertyName)}: ${this.wrapFieldValue(f)}`).join(", ");
|
|
88
|
+
return fields.length > 0
|
|
89
|
+
? `${typeName} { ${fields}, ..Default::default() }`
|
|
90
|
+
: `${typeName} { ..Default::default() }`;
|
|
91
|
+
}
|
|
92
|
+
const discValueStr = discValue.value;
|
|
93
|
+
// Find matching named child type
|
|
94
|
+
const childType = typeNode.childTypes.find(child => {
|
|
95
|
+
const dp = child.properties.find((p) => p.name === discFieldName);
|
|
96
|
+
return dp?.defaultValue === discValueStr;
|
|
97
|
+
});
|
|
98
|
+
let kindValue;
|
|
99
|
+
if (childType) {
|
|
100
|
+
// Named variant (e.g., PropertyKind::Array)
|
|
101
|
+
const variantName = childType.typeName.name.replace(typeName, '') || childType.typeName.name;
|
|
102
|
+
kindValue = `${enumName}::${variantName}`;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Wildcard/Custom variant — carries kind_name field
|
|
106
|
+
kindValue = `${enumName}::Custom { kind_name: "${discValueStr}".to_string() }`;
|
|
107
|
+
}
|
|
108
|
+
// Non-discriminator fields
|
|
109
|
+
const baseFields = expr.fields
|
|
110
|
+
.filter(f => f.propertyName !== discFieldName)
|
|
111
|
+
.map(f => `${toSnakeCase(f.propertyName)}: ${this.wrapFieldValue(f)}`);
|
|
112
|
+
const allFields = [
|
|
113
|
+
`${toSnakeCase(discFieldName)}: ${kindValue}`,
|
|
114
|
+
...baseFields,
|
|
115
|
+
];
|
|
116
|
+
return `${typeName} { ${allFields.join(", ")}, ..Default::default() }`;
|
|
117
|
+
}
|
|
118
|
+
visitVariant(expr) {
|
|
119
|
+
const baseName = expr.baseTypeName.name;
|
|
120
|
+
const variantName = expr.variantTypeName.name;
|
|
121
|
+
const enumName = `${baseName}Kind`;
|
|
122
|
+
const fields = expr.fields.map(f => `${toSnakeCase(f.propertyName)}: ${this.wrapFieldValue(f)}`).join(", ");
|
|
123
|
+
const kindValue = fields.length > 0
|
|
124
|
+
? `${enumName}::${variantName} { ${fields} }`
|
|
125
|
+
: `${enumName}::${variantName}`;
|
|
126
|
+
return `${baseName} { ${toSnakeCase(expr.discriminator)}: ${kindValue}, ..Default::default() }`;
|
|
127
|
+
}
|
|
128
|
+
visitArray(expr) {
|
|
129
|
+
if (expr.items.length === 0) {
|
|
130
|
+
return "vec![]";
|
|
131
|
+
}
|
|
132
|
+
const items = expr.items.map(i => this.visitExpr(i)).join(", ");
|
|
133
|
+
return `vec![${items}]`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Wrap field values appropriately for Rust — Option<T> fields need Some(),
|
|
137
|
+
* string fields need .into(), etc.
|
|
138
|
+
*/
|
|
139
|
+
wrapFieldValue(field) {
|
|
140
|
+
const inner = this.visitExpr(field.value);
|
|
141
|
+
if (field.isOptional) {
|
|
142
|
+
return `Some(${inner})`;
|
|
143
|
+
}
|
|
144
|
+
return inner;
|
|
145
|
+
}
|
|
146
|
+
escapeString(s) {
|
|
147
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
148
|
+
}
|
|
149
|
+
visitFieldRead(expr) {
|
|
150
|
+
return `${toSnakeCase(expr.objectName)}.${rustFieldName(expr.fieldName)}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=visitor.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EmitContext } from "@typespec/compiler";
|
|
2
|
+
import { EmitTarget, TypraEmitterOptions } from "../../lib.js";
|
|
3
|
+
import { TypeNode } from "../../ir/ast.js";
|
|
4
|
+
import { GeneratorOptions } from "../../emitter.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generate TypeScript code from TypeSpec models.
|
|
7
|
+
*/
|
|
8
|
+
export declare const generateTypeScript: (context: EmitContext<TypraEmitterOptions>, node: TypeNode, emitTarget: EmitTarget, options?: GeneratorOptions) => Promise<void>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { enumerateTypes } from "../../ir/ast.js";
|
|
3
|
+
import { filterNodes } from "../../emitter.js";
|
|
4
|
+
import { TypeRegistry } from "../../ir/expansion.js";
|
|
5
|
+
import { TypeScriptExprVisitor } from "./visitor.js";
|
|
6
|
+
import { emitTypeScriptFile as emitTypeScriptFileDecl } from "./emitter.js";
|
|
7
|
+
import { emitTypeScriptContext, emitTypeScriptIndex, emitTypeScriptGroupIndex, emitEslintConfig } from "./scaffolding.js";
|
|
8
|
+
import { emitTypeScriptTest } from "./test-emitter.js";
|
|
9
|
+
import { lowerFile, collectPolymorphicTypeNames } from "../../ir/lower.js";
|
|
10
|
+
import { buildBaseTestContext, typescriptTestOptions } from "../../testing/test-context.js";
|
|
11
|
+
import { toKebabCase } from "../../ir/utilities.js";
|
|
12
|
+
import { resolve, dirname } from "path";
|
|
13
|
+
import { execFileSync } from "child_process";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { emitGeneratedFile } from "../../cleanup/generated-file.js";
|
|
16
|
+
/**
|
|
17
|
+
* Stale-file deletion is intentionally disabled until manifest cleanup is enabled.
|
|
18
|
+
*/
|
|
19
|
+
function cleanupFlatTypeFiles(relDir, isTypeFile) {
|
|
20
|
+
void relDir;
|
|
21
|
+
void isTypeFile;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate TypeScript code from TypeSpec models.
|
|
26
|
+
*/
|
|
27
|
+
export const generateTypeScript = async (context, node, emitTarget, options) => {
|
|
28
|
+
const allTypes = Array.from(enumerateTypes(node));
|
|
29
|
+
const nodes = filterNodes(allTypes, options);
|
|
30
|
+
// Build the expression IR infrastructure
|
|
31
|
+
const registry = TypeRegistry.fromTypeGraph(allTypes);
|
|
32
|
+
const visitor = new TypeScriptExprVisitor(registry);
|
|
33
|
+
// Determine namespace: use override or default
|
|
34
|
+
const originalNamespace = node.typeName.namespace;
|
|
35
|
+
const tsNamespace = emitTarget.namespace ?? originalNamespace.replace(/\.Core$/, "");
|
|
36
|
+
// Stale flat-file cleanup is disabled in this slice.
|
|
37
|
+
cleanupFlatTypeFiles(emitTarget["output-dir"], name => name.endsWith(".ts") && name !== "context.ts" && name !== "index.ts" && name !== "eslint.config.js");
|
|
38
|
+
cleanupFlatTypeFiles(emitTarget["test-dir"], name => name.endsWith(".ts") && name !== "context.test.ts");
|
|
39
|
+
// Emit context classes (LoadContext, SaveContext)
|
|
40
|
+
const contextCode = emitTypeScriptContext();
|
|
41
|
+
await emitTypeScriptFile(context, "context.ts", contextCode, emitTarget["output-dir"]);
|
|
42
|
+
// Collect polymorphic type names once for the full type graph
|
|
43
|
+
const polymorphicTypeNames = new Set();
|
|
44
|
+
for (const n of allTypes) {
|
|
45
|
+
for (const name of collectPolymorphicTypeNames(n, registry)) {
|
|
46
|
+
polymorphicTypeNames.add(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Group root nodes by their semantic group folder
|
|
50
|
+
const groupMap = new Map();
|
|
51
|
+
for (const n of nodes) {
|
|
52
|
+
if (!n.base) {
|
|
53
|
+
const g = n.group || "";
|
|
54
|
+
if (!groupMap.has(g))
|
|
55
|
+
groupMap.set(g, []);
|
|
56
|
+
groupMap.get(g).push(n);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Emit each base type file (includes children in the same file)
|
|
60
|
+
for (const n of nodes) {
|
|
61
|
+
// Skip child types - they're rendered with their parent
|
|
62
|
+
if (n.base) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const group = n.group || "";
|
|
66
|
+
const fileDecl = lowerFile(n, registry, polymorphicTypeNames);
|
|
67
|
+
const code = emitTypeScriptFileDecl(fileDecl, visitor, tsNamespace, group);
|
|
68
|
+
const outDir = group ? `${emitTarget["output-dir"]}/${group}` : emitTarget["output-dir"];
|
|
69
|
+
await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.ts`, code, outDir);
|
|
70
|
+
}
|
|
71
|
+
// Emit group index.ts files
|
|
72
|
+
for (const [group, groupNodes] of groupMap) {
|
|
73
|
+
if (!group)
|
|
74
|
+
continue;
|
|
75
|
+
const groupIndexCode = emitTypeScriptGroupIndex(group, groupNodes);
|
|
76
|
+
await emitTypeScriptFile(context, "index.ts", groupIndexCode, `${emitTarget["output-dir"]}/${group}`);
|
|
77
|
+
}
|
|
78
|
+
// Emit test files for all types (skip protocols — they have no data to test)
|
|
79
|
+
if (emitTarget["test-dir"]) {
|
|
80
|
+
const importPath = emitTarget["import-path"] || "../src/index";
|
|
81
|
+
for (const n of nodes) {
|
|
82
|
+
if (n.isProtocol)
|
|
83
|
+
continue;
|
|
84
|
+
const group = n.group || "";
|
|
85
|
+
const testDir = group ? `${emitTarget["test-dir"]}/${group}` : emitTarget["test-dir"];
|
|
86
|
+
const groupDepth = group ? group.split("/").filter(Boolean).length : 0;
|
|
87
|
+
const testImportPath = groupDepth > 0 ? `${"../".repeat(groupDepth)}${importPath}` : importPath;
|
|
88
|
+
const testContext = buildTestContext(n);
|
|
89
|
+
const testCode = emitTypeScriptTest({
|
|
90
|
+
...testContext,
|
|
91
|
+
importPath: testImportPath,
|
|
92
|
+
namespace: tsNamespace,
|
|
93
|
+
});
|
|
94
|
+
await emitTypeScriptFile(context, `${toKebabCase(n.typeName.name)}.test.ts`, testCode, testDir);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Emit root index.ts file — re-exports from group sub-indexes
|
|
98
|
+
const indexContext = buildIndexContext(nodes);
|
|
99
|
+
const indexCode = emitTypeScriptIndex(indexContext.baseTypes, indexContext.types);
|
|
100
|
+
await emitTypeScriptFile(context, "index.ts", indexCode, emitTarget["output-dir"]);
|
|
101
|
+
// Emit eslint.config.js to project root (parent of output-dir)
|
|
102
|
+
if (emitTarget["output-dir"]) {
|
|
103
|
+
const projectRoot = resolve(process.cwd(), emitTarget["output-dir"], "..");
|
|
104
|
+
const eslintConfigCode = emitEslintConfig();
|
|
105
|
+
await emitTypeScriptFile(context, "eslint.config.js", eslintConfigCode, projectRoot);
|
|
106
|
+
}
|
|
107
|
+
// Format emitted files if format option is enabled (default: true)
|
|
108
|
+
if (emitTarget.format !== false) {
|
|
109
|
+
const outputDir = emitTarget["output-dir"]
|
|
110
|
+
? resolve(process.cwd(), emitTarget["output-dir"])
|
|
111
|
+
: context.emitterOutputDir;
|
|
112
|
+
const testDir = emitTarget["test-dir"]
|
|
113
|
+
? resolve(process.cwd(), emitTarget["test-dir"])
|
|
114
|
+
: undefined;
|
|
115
|
+
formatTypeScriptFiles(outputDir, testDir);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Format TypeScript files using prettier.
|
|
120
|
+
*/
|
|
121
|
+
function formatTypeScriptFiles(outputDir, testDir) {
|
|
122
|
+
const projectRoot = findTypeScriptProjectRoot(outputDir);
|
|
123
|
+
if (!projectRoot) {
|
|
124
|
+
console.warn(`Warning: Could not find package.json. Skipping formatting.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const dirs = [outputDir, ...(testDir ? [testDir] : [])];
|
|
128
|
+
const prettierBin = findNodeModuleFile(projectRoot, ["prettier", "bin", "prettier.cjs"]);
|
|
129
|
+
for (const dir of dirs) {
|
|
130
|
+
const globPattern = `${dir}/**/*.ts`;
|
|
131
|
+
if (prettierBin) {
|
|
132
|
+
try {
|
|
133
|
+
execFileSync(process.execPath, [prettierBin, "--write", globPattern], {
|
|
134
|
+
cwd: projectRoot,
|
|
135
|
+
stdio: "pipe",
|
|
136
|
+
encoding: "utf-8",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.warn(`Warning: prettier formatting failed for ${dir}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.warn(`Warning: prettier not found for ${dir}. Run npm install in the TypeScript workspace.`);
|
|
145
|
+
}
|
|
146
|
+
// Run eslint fix
|
|
147
|
+
try {
|
|
148
|
+
execFileSync("npx", ["eslint", "--fix", globPattern], {
|
|
149
|
+
cwd: projectRoot,
|
|
150
|
+
stdio: "pipe",
|
|
151
|
+
encoding: "utf-8",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
// ESLint errors are common, don't warn about them
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function findNodeModuleFile(startDir, segments) {
|
|
160
|
+
let currentDir = resolve(startDir);
|
|
161
|
+
const root = resolve("/");
|
|
162
|
+
while (currentDir !== root && currentDir !== dirname(currentDir)) {
|
|
163
|
+
const candidate = resolve(currentDir, "node_modules", ...segments);
|
|
164
|
+
if (existsSync(candidate)) {
|
|
165
|
+
return candidate;
|
|
166
|
+
}
|
|
167
|
+
currentDir = dirname(currentDir);
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Find the TypeScript project root by looking for package.json.
|
|
173
|
+
*/
|
|
174
|
+
function findTypeScriptProjectRoot(startDir) {
|
|
175
|
+
let currentDir = resolve(startDir);
|
|
176
|
+
const root = resolve("/");
|
|
177
|
+
while (currentDir !== root && currentDir !== dirname(currentDir)) {
|
|
178
|
+
const packageJsonPath = resolve(currentDir, "package.json");
|
|
179
|
+
if (existsSync(packageJsonPath)) {
|
|
180
|
+
return currentDir;
|
|
181
|
+
}
|
|
182
|
+
currentDir = dirname(currentDir);
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build context for rendering the index.ts file.
|
|
188
|
+
*/
|
|
189
|
+
function buildIndexContext(nodes) {
|
|
190
|
+
return {
|
|
191
|
+
baseTypes: nodes.filter((n) => !n.base),
|
|
192
|
+
types: nodes,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build context for rendering a test file.
|
|
197
|
+
*/
|
|
198
|
+
function buildTestContext(node) {
|
|
199
|
+
return buildBaseTestContext(node, undefined, typescriptTestOptions);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Write generated TypeScript content to file.
|
|
203
|
+
*/
|
|
204
|
+
async function emitTypeScriptFile(context, filename, content, outputDir) {
|
|
205
|
+
outputDir = outputDir || `${context.emitterOutputDir}/typescript`;
|
|
206
|
+
const filePath = resolvePath(outputDir, filename);
|
|
207
|
+
await emitGeneratedFile(context, filePath, content);
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript code emitter — Declaration IR → TypeScript source code.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `file.ts.njk` (~414 lines of Nunjucks templates)
|
|
5
|
+
* with a typed TypeScript function that walks the FileDecl tree.
|
|
6
|
+
*
|
|
7
|
+
* The emitter produces correctly-formatted TypeScript code using modern
|
|
8
|
+
* class patterns with `Partial<T>` constructors. Output is post-processed
|
|
9
|
+
* by prettier + eslint, so exact whitespace is not critical.
|
|
10
|
+
*
|
|
11
|
+
* Structural blocks emitted (in order):
|
|
12
|
+
* 1. Header comment (auto-generated warning)
|
|
13
|
+
* 2. Imports (LoadContext/SaveContext, referenced types)
|
|
14
|
+
* 3. For each type in the file:
|
|
15
|
+
* a. [abstract] class definition with extends
|
|
16
|
+
* b. shorthandProperty static ClassVar
|
|
17
|
+
* c. Field declarations with type annotations + defaults
|
|
18
|
+
* d. Constructor with Partial<T>
|
|
19
|
+
* e. //#region Load Methods
|
|
20
|
+
* - load() static method
|
|
21
|
+
* - loadKind() for polymorphic dispatch (private static)
|
|
22
|
+
* - Collection helpers (loadX / saveX)
|
|
23
|
+
* f. //#endregion
|
|
24
|
+
* g. //#region Save Methods
|
|
25
|
+
* - save() instance method
|
|
26
|
+
* - toYaml() / toJson()
|
|
27
|
+
* - fromJson() / fromYaml() static methods
|
|
28
|
+
* h. //#endregion
|
|
29
|
+
* i. Factory static methods
|
|
30
|
+
* j. Method stubs (as comments)
|
|
31
|
+
*/
|
|
32
|
+
import { FileDecl } from "../../ir/declarations.js";
|
|
33
|
+
import { ExprVisitor } from "../../ir/visitor.js";
|
|
34
|
+
/**
|
|
35
|
+
* Emit a complete TypeScript file from a FileDecl.
|
|
36
|
+
*
|
|
37
|
+
* @param decl - The file declaration to emit
|
|
38
|
+
* @param visitor - Expression visitor for rendering expressions
|
|
39
|
+
* @param namespace - Optional namespace override
|
|
40
|
+
* @param group - Semantic group folder this file lives in (e.g. "connection"). Empty string = root.
|
|
41
|
+
*/
|
|
42
|
+
export declare function emitTypeScriptFile(decl: FileDecl, visitor: ExprVisitor, namespace?: string, group?: string): string;
|