@needle-tools/needle-component-compiler 1.0.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/COMPILE.bat ADDED
@@ -0,0 +1 @@
1
+ npm run tsc src/component-compiler
package/INSTALL.bat ADDED
@@ -0,0 +1 @@
1
+ npm install && timeout 10
package/RUNTEST.bat ADDED
@@ -0,0 +1 @@
1
+ npm run tsc src/component-compiler && node src/component-compiler.js dist src/test.ts
package/Readme.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Typescript to Unity component
2
+
3
+ Little helper package to transpile typescript files to Unity C# components.
4
+ Please run ``npm install`` first before using.
5
+
6
+ ### Usage
7
+ ``node <path to>/component-compiler.js <output_directory> <path_to/my_script.ts>``
@@ -0,0 +1,11 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ // source: C:\git\needle-tiny-playground\modules\needle-tiny\component-compiler\src\test.ts
5
+ public class GltfExport : UnityEngine.MonoBehaviour
6
+ {
7
+ public bool @binary = true;
8
+ [UnityEngine.ContextMenu("enable this")]
9
+ public void test(){}
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ // source: C:\git\needle-tiny-playground\modules\needle-tiny\component-compiler\src\test.ts
5
+ public class GltfExportBox : UnityEngine.MonoBehaviour
6
+ {
7
+ public UnityEngine.Transform @sceneRoot;
8
+ public void start(){}
9
+ public void updateGltfBox(){}
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ public class MyArray : UnityEngine.MonoBehaviour
5
+ {
6
+ public float[] @arr = new float[]{ 1, 2, 3 };
7
+ }
8
+ }
@@ -0,0 +1,23 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ // source: C:\git\needle-tiny-playground\projects\Compiled_Export\myProject\src\scripts\MyClass.ts
5
+ public class MyClass : UnityEngine.MonoBehaviour
6
+ {
7
+ public void start(){}
8
+ public float myFloat = 15;
9
+ public bool myBool;
10
+ // just some default values
11
+ public float[] myArray = new float[]{ 1, 2, 3 };
12
+ // comment for myString
13
+ public string myString = "this is a string";
14
+ public UnityEngine.Transform myObject;
15
+ public bool myBool2;
16
+ public void myFunction(){}
17
+ public void myFunctionWithStringParameter(string @string){}
18
+ public void myFunctionWithSomeObjectAndArray(UnityEngine.Transform @obj, float[] @arr){}
19
+ public void myFunctionWithoutParamTypes(object @test){}
20
+ public UnityEngine.Transform[] someOtherStuff;
21
+ public UnityEngine.Events.UnityEvent myEvent;
22
+ }
23
+ }
@@ -0,0 +1,10 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ // source: C:\git\needle-tiny-playground\modules\needle-tiny\component-compiler\src\test.ts
5
+ public class MyClassWithAFloat : UnityEngine.MonoBehaviour
6
+ {
7
+ public float @myfloat = 0.5f;
8
+ private string @myString;
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ public class PrivateSerializedField : UnityEngine.MonoBehaviour
5
+ {
6
+ [UnityEngine.SerializeField]
7
+ private UnityEngine.Color @color;
8
+ }
9
+ }
package/dist/Test.cs ADDED
@@ -0,0 +1,8 @@
1
+ // auto generated code - do not edit
2
+ namespace Needle.Typescript.GeneratedComponents
3
+ {
4
+ // source: C:\git\needle-tiny-playground\projects\Compiled_Export\myProject\src\scripts\SomeTestComponent.ts
5
+ public class Test : UnityEngine.MonoBehaviour
6
+ {
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@needle-tools/needle-component-compiler",
3
+ "version": "1.0.0",
4
+ "description": "Compile mock unity components from typescript",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "tsc": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "typescript": "^4.5.5"
11
+ },
12
+ "devDependencies": {
13
+ "@types/node": "^17.0.16"
14
+ },
15
+ "author": {
16
+ "name": "Needle",
17
+ "email": "help@needle.tools",
18
+ "url": "https://needle.tools/"
19
+ },
20
+ "license": "ISC"
21
+ }
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ exports.__esModule = true;
3
+ exports.run = void 0;
4
+ var fs_1 = require("fs");
5
+ var ts = require("typescript");
6
+ var fs = require("fs");
7
+ var types = require("./types");
8
+ var dict = types.dict;
9
+ // add either of these two comments above a class to enforce code gen or disable it for the next class
10
+ var exportNextClassCommand = "@generate-component";
11
+ var dontExportNextClassCommand = "@dont-generate-component";
12
+ // add above field to add [SerializeField] attribute
13
+ var serializeCommand = "@serializeField";
14
+ // will be set to true when e.g. a comment for export is found
15
+ var exportNextClass = false;
16
+ var dontExportNextClass = false;
17
+ var serializeField = false;
18
+ function resetExportNextClass() {
19
+ dontExportNextClass = false;
20
+ exportNextClass = false;
21
+ }
22
+ // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
23
+ // const exportDir = "../dist";
24
+ var commentStarts = [];
25
+ var ExportContext = /** @class */ (function () {
26
+ function ExportContext(outputDir, fileName) {
27
+ this.classEnd = -1;
28
+ this.indentLevel = 0;
29
+ this.emitMethodContextMenu = null;
30
+ this.outputDir = outputDir;
31
+ this.fileName = fileName;
32
+ this.reset();
33
+ }
34
+ ExportContext.prototype.onBeforeMethod = function (name) {
35
+ if (this.emitMethodContextMenu !== undefined) {
36
+ var contextMenuText = this.emitMethodContextMenu === null ? name : this.emitMethodContextMenu;
37
+ this.appendLine("[UnityEngine.ContextMenu(\"" + contextMenuText + "\")]");
38
+ }
39
+ };
40
+ ExportContext.prototype.append = function (text) {
41
+ for (var i = 0; i < this.indentLevel; i++)
42
+ text = "\t" + text;
43
+ this.textBuffer += text;
44
+ this.emitMethodContextMenu = undefined;
45
+ };
46
+ ExportContext.prototype.appendLine = function (text) {
47
+ this.append(text + "\n");
48
+ };
49
+ ExportContext.prototype.flush = function () {
50
+ if (this.textBuffer.length <= 0)
51
+ return;
52
+ var dir = this.outputDir + "/";
53
+ if (!fs.existsSync(dir)) {
54
+ fs.mkdirSync(dir);
55
+ }
56
+ var path = dir + this.fileName;
57
+ console.log("Write to " + path);
58
+ fs.writeFileSync(path, this.textBuffer);
59
+ this.reset();
60
+ };
61
+ ExportContext.prototype.reset = function () {
62
+ this.textBuffer = "";
63
+ this.classEnd = -1;
64
+ };
65
+ return ExportContext;
66
+ }());
67
+ var contexts = [];
68
+ function run(program, outputDir, sourceFile) {
69
+ if (!fs.existsSync(outputDir)) {
70
+ console.error("Output directory does not exist: \"" + outputDir + "\"");
71
+ return;
72
+ }
73
+ traverseFile(sourceFile);
74
+ function traverseFile(node) {
75
+ visit(node);
76
+ ts.forEachChild(node, traverseFile);
77
+ }
78
+ function visit(node) {
79
+ var _a, _b, _c;
80
+ var context = contexts.length > 0 ? contexts[contexts.length - 1] : null;
81
+ if ((context === null || context === void 0 ? void 0 : context.classEnd) > 0 && node.pos >= (context === null || context === void 0 ? void 0 : context.classEnd)) {
82
+ while (context.indentLevel > 0) {
83
+ context.indentLevel -= 1;
84
+ context.append("}\n");
85
+ }
86
+ context.flush();
87
+ context = null;
88
+ contexts.pop();
89
+ }
90
+ console.log("\t", ts.SyntaxKind[node.kind]);
91
+ var commentRanges = ts.getLeadingCommentRanges(sourceFile.getFullText(), node.getFullStart());
92
+ if (commentRanges === null || commentRanges === void 0 ? void 0 : commentRanges.length) {
93
+ for (var _i = 0, commentRanges_1 = commentRanges; _i < commentRanges_1.length; _i++) {
94
+ var r = commentRanges_1[_i];
95
+ // avoid emitting comments multiple times
96
+ if (commentStarts.includes(r.pos))
97
+ continue;
98
+ commentStarts.push(r.pos);
99
+ var comment = node.getSourceFile().getFullText().slice(r.pos, r.end);
100
+ if (context) {
101
+ // https://regex101.com/r/ud4oev/1
102
+ var emitContextMenu = comment.match("(\/{2,}|\/\*)\s*@contextmenu {0,}(?<text>[a-zA-Z 0-9]+)?");
103
+ if (emitContextMenu) {
104
+ context.emitMethodContextMenu = (_b = (_a = emitContextMenu.groups) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : null;
105
+ }
106
+ else if (comment.includes(serializeCommand))
107
+ serializeField = true;
108
+ else
109
+ context.appendLine(comment);
110
+ }
111
+ if (comment.includes(exportNextClassCommand))
112
+ exportNextClass = true;
113
+ if (comment.includes(dontExportNextClassCommand))
114
+ dontExportNextClass = true;
115
+ }
116
+ }
117
+ switch (node.kind) {
118
+ // case ts.SyntaxKind.ClassDeclaration:
119
+ // case ts.SyntaxKind.SingleLineCommentTrivia:
120
+ // console.log("comment: " + node.getText())
121
+ // break;
122
+ case ts.SyntaxKind.Decorator:
123
+ break;
124
+ case ts.SyntaxKind.MethodDeclaration:
125
+ serializeField = false;
126
+ resetExportNextClass();
127
+ if (!context)
128
+ break;
129
+ // TODO: always emit at least OnEnable method per class so generated components have toggle in editor
130
+ var meth = node;
131
+ // const isCoroutine = func.asteriskToken;
132
+ if (meth.name) {
133
+ var paramsStr = "";
134
+ for (var _d = 0, _e = meth.parameters; _d < _e.length; _d++) {
135
+ var param = _e[_d];
136
+ if (!param || !param.name)
137
+ continue;
138
+ if (paramsStr.length > 0)
139
+ paramsStr += ", ";
140
+ var type = tryResolveTypeRecursive(param);
141
+ if (type === undefined)
142
+ type = "object";
143
+ paramsStr += type + " @" + param.name.getText();
144
+ }
145
+ var methodName = meth.name.getText();
146
+ context.onBeforeMethod(methodName);
147
+ context.append("public void " + methodName + "(" + paramsStr + "){}\n");
148
+ }
149
+ break;
150
+ case ts.SyntaxKind.SetAccessor:
151
+ case ts.SyntaxKind.PropertyDeclaration:
152
+ resetExportNextClass();
153
+ if (!context)
154
+ break;
155
+ console.log("Found variable", node.getText());
156
+ var vardec = node;
157
+ var name_1 = vardec.name.getText();
158
+ console.log(name_1);
159
+ if (name_1.startsWith("\"@") || name_1.startsWith("\"$") || name_1.startsWith("$"))
160
+ break;
161
+ var typeString = tryResolveTypeRecursive(node);
162
+ if (typeString === undefined) {
163
+ context.append("// Could not resolve type \"" + ((_c = vardec.type) === null || _c === void 0 ? void 0 : _c.getText()) + "\"\n");
164
+ }
165
+ var prefix = typeString === undefined ? "// " : "";
166
+ var assignment = "";
167
+ if (typeString !== undefined) {
168
+ for (var _f = 0, _g = node.getChildren(); _f < _g.length; _f++) {
169
+ var ch = _g[_f];
170
+ switch (ch.kind) {
171
+ case ts.SyntaxKind.FalseKeyword:
172
+ case ts.SyntaxKind.TrueKeyword:
173
+ assignment = " = " + ch.getText();
174
+ break;
175
+ case ts.SyntaxKind.StringLiteral:
176
+ var str = ch;
177
+ assignment = " = \"" + str.text + "\"";
178
+ break;
179
+ case ts.SyntaxKind.FirstLiteralToken:
180
+ var lit = ch;
181
+ assignment = " = " + lit.text;
182
+ if (ts.isNumericLiteral(lit))
183
+ assignment += "f";
184
+ break;
185
+ case ts.SyntaxKind.ArrayLiteralExpression:
186
+ var arr = ch;
187
+ assignment = " = new " + typeString + "{" + arr.elements.map(function (e) { return " " + e.getText(); }) + " }";
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ var varName = "@" + vardec.name.getText();
193
+ var pub = isPublic(vardec);
194
+ var visibility = pub ? "public" : "private";
195
+ if (!pub && serializeField) {
196
+ console.log("SERIALIZE");
197
+ context.appendLine("[UnityEngine.SerializeField]");
198
+ }
199
+ context.append(prefix + visibility + " " + typeString + " " + varName + assignment + ";\n");
200
+ break;
201
+ case ts.SyntaxKind.ClassDeclaration:
202
+ serializeField = false;
203
+ var dec = node;
204
+ // a class must inherit a component
205
+ if (!dontExportNextClass && (exportNextClass || testInheritsComponent(node))) {
206
+ resetExportNextClass();
207
+ var name_2 = dec.name.escapedText;
208
+ console.log("Found class: ", name_2);
209
+ var newContext = new ExportContext(outputDir, name_2 + ".cs");
210
+ newContext.appendLine("// auto generated code - do not edit");
211
+ newContext.appendLine("namespace Needle.Typescript.GeneratedComponents");
212
+ newContext.appendLine("{");
213
+ newContext.indentLevel += 1;
214
+ // newContext.appendLine("// source: " + path.resolve(sourceFile.fileName));
215
+ newContext.appendLine("public class " + name_2 + " : UnityEngine.MonoBehaviour");
216
+ newContext.appendLine("{");
217
+ newContext.indentLevel += 1;
218
+ newContext.classEnd = dec.end;
219
+ contexts.push(newContext);
220
+ }
221
+ break;
222
+ }
223
+ function testInheritsComponent(node) {
224
+ switch (node.kind) {
225
+ case ts.SyntaxKind.ClassDeclaration:
226
+ var dec = node;
227
+ if (dec.heritageClauses) {
228
+ for (var _i = 0, _a = dec.heritageClauses; _i < _a.length; _i++) {
229
+ var h = _a[_i];
230
+ if (h.types.length <= 0)
231
+ continue;
232
+ for (var _b = 0, _c = h.types; _b < _c.length; _b++) {
233
+ var type = _c[_b];
234
+ // const symbol = program.getTypeChecker().getSymbolAtLocation(type.expression);
235
+ // console.log(symbol);
236
+ if (type.expression.getText() === "Component")
237
+ return true;
238
+ if (type.expression.getText() === "Behaviour")
239
+ return true;
240
+ }
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+ return false;
246
+ }
247
+ function isPublic(node) {
248
+ if (node.kind === ts.SyntaxKind.PublicKeyword) {
249
+ return true;
250
+ }
251
+ else if (node.kind === ts.SyntaxKind.PrivateKeyword) {
252
+ return false;
253
+ }
254
+ for (var _i = 0, _a = node.getChildren(); _i < _a.length; _i++) {
255
+ var ch = _a[_i];
256
+ if (!isPublic(ch))
257
+ return false;
258
+ }
259
+ return true;
260
+ }
261
+ function tryResolveTypeRecursive(node) {
262
+ var _a;
263
+ if (!node)
264
+ return undefined;
265
+ // skip decorators (e.g. @serializable() may break array generation)
266
+ if (node.kind === ts.SyntaxKind.Decorator)
267
+ return undefined;
268
+ var typeName = node === null || node === void 0 ? void 0 : node.getText();
269
+ var varDec = node;
270
+ if (varDec.type) {
271
+ typeName = varDec.type.getText();
272
+ }
273
+ var res = dict[typeName];
274
+ if (res !== undefined) {
275
+ // console.log("FOUND " + res, ts.SyntaxKind[node.kind]);
276
+ // console.log(node);
277
+ return res;
278
+ }
279
+ // console.log("Unknown type: " + typeName);
280
+ switch (node.kind) {
281
+ case ts.SyntaxKind.SyntaxList:
282
+ // const list = node as ts.SyntaxList;
283
+ // for(const ch of list._children)
284
+ // res = tryResolveTypeRecursive(ch);
285
+ break;
286
+ case ts.SyntaxKind.UnionType:
287
+ var union = node;
288
+ for (var _i = 0, _b = union.types; _i < _b.length; _i++) {
289
+ var t = _b[_i];
290
+ res = tryResolveTypeRecursive(t);
291
+ if (res !== undefined)
292
+ return res;
293
+ }
294
+ break;
295
+ case ts.SyntaxKind.ArrayType:
296
+ var arrayType = node;
297
+ var typeName_1 = (_a = arrayType.elementType) === null || _a === void 0 ? void 0 : _a.getText();
298
+ if (typeName_1 !== undefined) {
299
+ res = dict[typeName_1];
300
+ if (res !== undefined) {
301
+ res += "[]";
302
+ return res;
303
+ }
304
+ }
305
+ }
306
+ for (var _c = 0, _d = node.getChildren(); _c < _d.length; _c++) {
307
+ var child = _d[_c];
308
+ if (res !== undefined)
309
+ break;
310
+ console.log("Child type: " + ts.SyntaxKind[child.kind]);
311
+ res = tryResolveTypeRecursive(child);
312
+ }
313
+ if (ts.isTypeReferenceNode(node)) {
314
+ var typeRef = node;
315
+ var typeName_2 = typeRef.typeName.getText();
316
+ switch (typeName_2) {
317
+ case "Array":
318
+ res += "[]";
319
+ return res;
320
+ }
321
+ }
322
+ return res;
323
+ }
324
+ }
325
+ }
326
+ exports.run = run;
327
+ if (process.argv.length < 4) {
328
+ console.error("Missing args, call with: <output_dir> <input_files>");
329
+ }
330
+ else {
331
+ var outputDir_1 = process.argv[2];
332
+ var fileNames = process.argv.slice(3);
333
+ fileNames.forEach(function (fileName) {
334
+ // Parse a file
335
+ var sourceFile = ts.createSourceFile(fileName, (0, fs_1.readFileSync)(fileName).toString(), ts.ScriptTarget.ES2015,
336
+ /*setParentNodes */ true);
337
+ var prog = ts.createProgram([fileName], {});
338
+ // delint it
339
+ run(prog, outputDir_1, sourceFile);
340
+ });
341
+ }
@@ -0,0 +1,362 @@
1
+ import { readFileSync } from "fs";
2
+ import * as ts from "typescript";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+
6
+ import * as types from "./types";
7
+ const dict = types.dict;
8
+
9
+ // add either of these two comments above a class to enforce code gen or disable it for the next class
10
+ const exportNextClassCommand = "@generate-component";
11
+ const dontExportNextClassCommand = "@dont-generate-component";
12
+ // add above field to add [SerializeField] attribute
13
+ const serializeCommand = "@serializeField";
14
+
15
+ // will be set to true when e.g. a comment for export is found
16
+ let exportNextClass: boolean = false;
17
+ let dontExportNextClass: boolean = false;
18
+ let serializeField: boolean = false;
19
+ function resetExportNextClass() {
20
+ dontExportNextClass = false;
21
+ exportNextClass = false;
22
+ }
23
+
24
+
25
+ // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
26
+
27
+ // const exportDir = "../dist";
28
+ const commentStarts: Array<number> = [];
29
+
30
+ class ExportContext {
31
+ outputDir: string;
32
+ fileName: string;
33
+ textBuffer: string;
34
+ classEnd: number = -1;
35
+ indentLevel: number = 0;
36
+
37
+ constructor(outputDir: string, fileName: string) {
38
+ this.outputDir = outputDir;
39
+ this.fileName = fileName;
40
+ this.reset();
41
+ }
42
+
43
+ emitMethodContextMenu: string | null | undefined = null;
44
+
45
+ onBeforeMethod(name: string) {
46
+ if (this.emitMethodContextMenu !== undefined) {
47
+ const contextMenuText = this.emitMethodContextMenu === null ? name : this.emitMethodContextMenu;
48
+ this.appendLine("[UnityEngine.ContextMenu(\"" + contextMenuText + "\")]");
49
+ }
50
+ }
51
+
52
+ append(text: string) {
53
+ for (let i = 0; i < this.indentLevel; i++)
54
+ text = "\t" + text;
55
+ this.textBuffer += text;
56
+ this.emitMethodContextMenu = undefined;
57
+ }
58
+
59
+ appendLine(text: string) {
60
+ this.append(text + "\n");
61
+ }
62
+
63
+ flush() {
64
+ if (this.textBuffer.length <= 0) return;
65
+ const dir = this.outputDir + "/";
66
+ if (!fs.existsSync(dir)) {
67
+ fs.mkdirSync(dir);
68
+ }
69
+ const path = dir + this.fileName;
70
+ console.log("Write to " + path);
71
+ fs.writeFileSync(path, this.textBuffer);
72
+ this.reset();
73
+ }
74
+
75
+ reset() {
76
+ this.textBuffer = "";
77
+ this.classEnd = -1;
78
+ }
79
+ }
80
+
81
+ const contexts: ExportContext[] = [];
82
+
83
+ export function run(program: ts.Program, outputDir: string, sourceFile: ts.SourceFile) {
84
+
85
+ if (!fs.existsSync(outputDir)) {
86
+ console.error("Output directory does not exist: \"" + outputDir + "\"");
87
+ return;
88
+ }
89
+
90
+ traverseFile(sourceFile);
91
+ function traverseFile(node: ts.Node) {
92
+
93
+ visit(node);
94
+ ts.forEachChild(node, traverseFile);
95
+ }
96
+
97
+ function visit(node: ts.Node) {
98
+ let context: ExportContext | null = contexts.length > 0 ? contexts[contexts.length - 1] : null;
99
+
100
+ if (context?.classEnd > 0 && node.pos >= context?.classEnd) {
101
+ while (context.indentLevel > 0) {
102
+ context.indentLevel -= 1;
103
+ context.append("}\n");
104
+ }
105
+ context.flush();
106
+ context = null;
107
+ contexts.pop();
108
+ }
109
+ console.log("\t", ts.SyntaxKind[node.kind]);
110
+
111
+ const commentRanges = ts.getLeadingCommentRanges(
112
+ sourceFile.getFullText(),
113
+ node.getFullStart());
114
+ if (commentRanges?.length) {
115
+ for (const r of commentRanges) {
116
+ // avoid emitting comments multiple times
117
+ if (commentStarts.includes(r.pos)) continue;
118
+ commentStarts.push(r.pos);
119
+ const comment = node.getSourceFile().getFullText().slice(r.pos, r.end);
120
+ if (context) {
121
+ // https://regex101.com/r/ud4oev/1
122
+ const emitContextMenu = comment.match("(\/{2,}|\/\*)\s*@contextmenu {0,}(?<text>[a-zA-Z 0-9]+)?");
123
+ if (emitContextMenu) {
124
+ context.emitMethodContextMenu = emitContextMenu.groups?.text ?? null;
125
+ }
126
+ else if (comment.includes(serializeCommand))
127
+ serializeField = true;
128
+ else
129
+ context.appendLine(comment);
130
+ }
131
+ if (comment.includes(exportNextClassCommand))
132
+ exportNextClass = true;
133
+ if (comment.includes(dontExportNextClassCommand))
134
+ dontExportNextClass = true;
135
+ }
136
+ }
137
+
138
+ switch (node.kind) {
139
+ // case ts.SyntaxKind.ClassDeclaration:
140
+ // case ts.SyntaxKind.SingleLineCommentTrivia:
141
+ // console.log("comment: " + node.getText())
142
+ // break;
143
+ case ts.SyntaxKind.Decorator:
144
+ break;
145
+ case ts.SyntaxKind.MethodDeclaration:
146
+ serializeField = false;
147
+ resetExportNextClass();
148
+ if (!context) break;
149
+ // TODO: always emit at least OnEnable method per class so generated components have toggle in editor
150
+ const meth = node as ts.MethodDeclaration;
151
+ // const isCoroutine = func.asteriskToken;
152
+ if (meth.name) {
153
+ let paramsStr = "";
154
+ for (let param of meth.parameters) {
155
+ if (!param || !param.name) continue;
156
+ if (paramsStr.length > 0) paramsStr += ", ";
157
+ let type = tryResolveTypeRecursive(param);
158
+ if (type === undefined) type = "object";
159
+ paramsStr += type + " @" + param.name.getText();
160
+ }
161
+ const methodName = meth.name.getText();
162
+ context.onBeforeMethod(methodName);
163
+ context.append("public void " + methodName + "(" + paramsStr + "){}\n");
164
+ }
165
+ break;
166
+
167
+
168
+ case ts.SyntaxKind.SetAccessor:
169
+ case ts.SyntaxKind.PropertyDeclaration:
170
+ resetExportNextClass();
171
+ if (!context) break;
172
+ console.log("Found variable", node.getText());
173
+ const vardec = node as ts.VariableDeclaration;
174
+ const name = vardec.name.getText();
175
+ console.log(name);
176
+ if (name.startsWith("\"@") || name.startsWith("\"$") || name.startsWith("$")) break;
177
+ let typeString = tryResolveTypeRecursive(node);
178
+ if (typeString === undefined) {
179
+ context.append("// Could not resolve type \"" + vardec.type?.getText() + "\"\n");
180
+ }
181
+ const prefix = typeString === undefined ? "// " : "";
182
+ let assignment = "";
183
+ if (typeString !== undefined) {
184
+ for (const ch of node.getChildren()) {
185
+ switch (ch.kind) {
186
+ case ts.SyntaxKind.FalseKeyword:
187
+ case ts.SyntaxKind.TrueKeyword:
188
+ assignment = " = " + ch.getText();
189
+ break;
190
+ case ts.SyntaxKind.StringLiteral:
191
+ const str = ch as ts.StringLiteral;
192
+ assignment = " = \"" + str.text + "\"";
193
+ break;
194
+ case ts.SyntaxKind.FirstLiteralToken:
195
+ const lit = ch as ts.LiteralExpression;
196
+ assignment = " = " + lit.text;
197
+ if (ts.isNumericLiteral(lit))
198
+ assignment += "f";
199
+ break;
200
+ case ts.SyntaxKind.ArrayLiteralExpression:
201
+ const arr = ch as ts.ArrayLiteralExpression;
202
+ assignment = " = new " + typeString + "{" + arr.elements.map(e => " " + e.getText()) + " }"
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ const varName = "@" + vardec.name.getText();
208
+ const pub = isPublic(vardec);
209
+ const visibility = pub ? "public" : "private";
210
+ if (!pub && serializeField) {
211
+ console.log("SERIALIZE");
212
+ context.appendLine("[UnityEngine.SerializeField]");
213
+ }
214
+ context.append(prefix + visibility + " " + typeString + " " + varName + assignment + ";\n");
215
+ break;
216
+
217
+ case ts.SyntaxKind.ClassDeclaration:
218
+ serializeField = false;
219
+ const dec = <ts.ClassDeclaration>node;
220
+ // a class must inherit a component
221
+ if (!dontExportNextClass && (exportNextClass || testInheritsComponent(node))) {
222
+ resetExportNextClass();
223
+ const name = dec.name.escapedText;
224
+ console.log("Found class: ", name);
225
+ const newContext = new ExportContext(outputDir, name + ".cs");
226
+ newContext.appendLine("// auto generated code - do not edit")
227
+ newContext.appendLine("namespace Needle.Typescript.GeneratedComponents");
228
+ newContext.appendLine("{");
229
+ newContext.indentLevel += 1;
230
+ // newContext.appendLine("// source: " + path.resolve(sourceFile.fileName));
231
+ newContext.appendLine("public class " + name + " : UnityEngine.MonoBehaviour");
232
+ newContext.appendLine("{");
233
+ newContext.indentLevel += 1;
234
+ newContext.classEnd = dec.end;
235
+ contexts.push(newContext);
236
+ }
237
+ break;
238
+ }
239
+
240
+ function testInheritsComponent(node: ts.Node): boolean {
241
+ switch (node.kind) {
242
+ case ts.SyntaxKind.ClassDeclaration:
243
+ const dec = <ts.ClassDeclaration>node;
244
+ if (dec.heritageClauses) {
245
+ for (const h of dec.heritageClauses) {
246
+ if (h.types.length <= 0) continue;
247
+ for (const type of h.types) {
248
+ // const symbol = program.getTypeChecker().getSymbolAtLocation(type.expression);
249
+ // console.log(symbol);
250
+ if (type.expression.getText() === "Component") return true;
251
+ if (type.expression.getText() === "Behaviour") return true;
252
+ }
253
+ }
254
+ }
255
+ return false;
256
+ }
257
+ return false;
258
+ }
259
+
260
+ function isPublic(node: ts.Node): boolean {
261
+ if (node.kind === ts.SyntaxKind.PublicKeyword) {
262
+ return true;
263
+ }
264
+ else if (node.kind === ts.SyntaxKind.PrivateKeyword) {
265
+ return false;
266
+ }
267
+
268
+ for (const ch of node.getChildren()) {
269
+ if (!isPublic(ch)) return false;
270
+ }
271
+ return true;
272
+ }
273
+
274
+ function tryResolveTypeRecursive(node: ts.Node | ts.VariableDeclaration): string | undefined {
275
+ if (!node) return undefined;
276
+
277
+ // skip decorators (e.g. @serializable() may break array generation)
278
+ if(node.kind === ts.SyntaxKind.Decorator)
279
+ return undefined;
280
+
281
+ let typeName = node?.getText();
282
+
283
+ const varDec: ts.VariableDeclaration = node as ts.VariableDeclaration;
284
+ if (varDec.type) {
285
+ typeName = varDec.type.getText();
286
+ }
287
+
288
+ let res = dict[typeName];
289
+ if (res !== undefined) {
290
+ // console.log("FOUND " + res, ts.SyntaxKind[node.kind]);
291
+ // console.log(node);
292
+ return res;
293
+ }
294
+ // console.log("Unknown type: " + typeName);
295
+
296
+ switch (node.kind) {
297
+ case ts.SyntaxKind.SyntaxList:
298
+ // const list = node as ts.SyntaxList;
299
+ // for(const ch of list._children)
300
+ // res = tryResolveTypeRecursive(ch);
301
+ break;
302
+ case ts.SyntaxKind.UnionType:
303
+ const union = node as ts.UnionTypeNode;
304
+ for (const t of union.types) {
305
+ res = tryResolveTypeRecursive(t);
306
+ if (res !== undefined) return res;
307
+ }
308
+ break;
309
+ case ts.SyntaxKind.ArrayType:
310
+ const arrayType = <ts.ArrayTypeNode>node;
311
+ const typeName = arrayType.elementType?.getText();
312
+ if (typeName !== undefined) {
313
+ res = dict[typeName];
314
+ if (res !== undefined) {
315
+ res += "[]";
316
+ return res;
317
+ }
318
+ }
319
+ }
320
+
321
+ for (const child of node.getChildren()) {
322
+ if (res !== undefined) break;
323
+ console.log("Child type: " + ts.SyntaxKind[child.kind]);
324
+ res = tryResolveTypeRecursive(child);
325
+ }
326
+
327
+ if(ts.isTypeReferenceNode(node)){
328
+ const typeRef = node as ts.TypeReferenceNode;
329
+ const typeName = typeRef.typeName.getText();
330
+ switch(typeName)
331
+ {
332
+ case "Array":
333
+ res += "[]";
334
+ return res;
335
+ }
336
+ }
337
+ return res;
338
+ }
339
+ }
340
+ }
341
+
342
+ if (process.argv.length < 4) {
343
+ console.error("Missing args, call with: <output_dir> <input_files>");
344
+ }
345
+ else {
346
+ const outputDir = process.argv[2];
347
+ const fileNames = process.argv.slice(3);
348
+ fileNames.forEach(fileName => {
349
+ // Parse a file
350
+ const sourceFile = ts.createSourceFile(
351
+ fileName,
352
+ readFileSync(fileName).toString(),
353
+ ts.ScriptTarget.ES2015,
354
+ /*setParentNodes */ true
355
+ );
356
+
357
+ const prog = ts.createProgram([fileName], {});
358
+
359
+ // delint it
360
+ run(prog, outputDir, sourceFile);
361
+ });
362
+ }
@@ -0,0 +1 @@
1
+ public class MyClass : MonoBehaviour {
package/src/test.ts ADDED
@@ -0,0 +1,97 @@
1
+
2
+ // @generate-component
3
+
4
+
5
+ export class MaterialColorHandler extends Behaviour {
6
+
7
+ @serializeable(Renderer)
8
+ renderer?: Renderer[];
9
+ }
10
+
11
+ // export class MyArray extends Behaviour {
12
+
13
+ // arr? : Array<number> = [1,2,3];
14
+ // }
15
+
16
+ // export class PrivateSerializedField extends Behaviour {
17
+
18
+ // //@serializeField
19
+ // private color? : THREE.Color;
20
+ // }
21
+ // export class MyPropertyClass extends Behaviour {
22
+ // set color(col: THREE.Color) {
23
+ // }
24
+ // }
25
+
26
+ // export class MyClassWithAFloat extends Behaviour {
27
+ // myfloat:number = .5;
28
+ // private myString : string;
29
+ // }
30
+
31
+ // export class GltfExport extends Behaviour {
32
+ // binary: boolean = true;
33
+ // "$serializedTypes" = {
34
+ // url:null
35
+ // }
36
+
37
+ // // @contextmenu enable this
38
+ // test(){
39
+
40
+ // }
41
+ // }
42
+
43
+ // export class SetColor extends Behaviour {
44
+
45
+ // "@serializedTypes" = {
46
+ // col: Number,
47
+ // }
48
+ // }
49
+
50
+ // class Behaviour {
51
+
52
+ // }
53
+
54
+ // export class PlatformerMusic extends Behaviour implements IPlaymodeChangeListener {
55
+ // source?: AudioSource;
56
+ // editMode?: string;
57
+ // playMode?: string;
58
+
59
+ // onPlaymodeChange(playmode: PlayMode): void {
60
+ // console.log(this);
61
+ // if(!this.source) return;
62
+ // const clip = playmode.isInPlayMode ? this.playMode : this.editMode;
63
+ // console.log("PLAY", clip);
64
+ // this.source.play(clip);
65
+ // }
66
+
67
+ // }
68
+
69
+ // // TODO: export UnityEvent like this
70
+ // // disable codegen
71
+ // class UnityEvent {
72
+ // methods: Function[] = [];
73
+ // invoke() {
74
+ // for (const m of this.methods) {
75
+ // m();
76
+ // }
77
+ // }
78
+ // }
79
+
80
+ // export class MyClass extends Behaviour {
81
+ // myFloat: number = 15;
82
+ // myBool: boolean;
83
+ // // just some default values
84
+ // myArray: number[] = [1, 2, 3];
85
+ // // comment for myString
86
+ // myString: string = "this is a string1";
87
+ // myObject: THREE.Object3D;
88
+ // myBool2: boolean;
89
+
90
+ // myFunction() { }
91
+ // myFunctionWithStringParameter(string: string) { }
92
+ // myFunctionWithSomeObjectAndArray(obj: THREE.Object3D, arr: number[]) { }
93
+ // myFunctionWithoutParamTypes(test) { }
94
+
95
+ // someOtherStuff: THREE.Object3D[] | null = null;
96
+ // myEvent: UnityEvent;
97
+ // }
package/src/types.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ exports.__esModule = true;
3
+ exports.delint = void 0;
4
+
5
+ const dict = {
6
+ "number": "float",
7
+ "string": "string",
8
+ "boolean": "bool",
9
+ // basic
10
+ "Behaviour" : "UnityEngine.Behaviour",
11
+ "Component" : "UnityEngine.Component",
12
+ "GameObject" : "UnityEngine.GameObject",
13
+ "Object3D" : "UnityEngine.Transform",
14
+ // Addressables
15
+ "AssetReference" : "UnityEngine.Transform",
16
+ // events
17
+ "EventList" : "UnityEngine.Events.UnityEvent",
18
+ // audio
19
+ "AudioClip" : "UnityEngine.AudioClip",
20
+ "THREE.Object3D" : "UnityEngine.Transform",
21
+ "UnityEvent" : "UnityEngine.Events.UnityEvent",
22
+ "AudioSource" : "UnityEngine.AudioSource",
23
+ "THREE.Color" : "UnityEngine.Color",
24
+ "Color" : "UnityEngine.Color",
25
+ "THREE.Vector2" : "UnityEngine.Vector2",
26
+ "Vector2" : "UnityEngine.Vector2",
27
+ "THREE.Vector3" : "UnityEngine.Vector3",
28
+ "Vector3" : "UnityEngine.Vector3",
29
+ "THREE.Vector4" : "UnityEngine.Vector4",
30
+ "Vector4" : "UnityEngine.Vector4",
31
+ // animation
32
+ "Animator" : "UnityEngine.Animator",
33
+ "Animation" : "UnityEngine.Animation",
34
+ "AnimationClip" : "UnityEngine.AnimationClip",
35
+ "SignalAsset" : "UnityEngine.Timeline.SignalAsset",
36
+ "PlayableDirector" : "UnityEngine.Playables.PlayableDirector",
37
+ // Rendering
38
+ "Renderer" : "UnityEngine.Renderer",
39
+ "Material" : "UnityEngine.Material",
40
+ // UI
41
+ "Text" : "UnityEngine.UI.Text",
42
+ "Image" : "UnityEngine.UI.Image",
43
+ "RawImage": "UnityEngine.UI.RawImage",
44
+
45
+ }
46
+
47
+ exports.dict = dict;
@@ -0,0 +1,8 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ }
6
+ ],
7
+ "settings": {}
8
+ }