From 108f9bf3553676b04e4de8c101aff9d9bd2e8af6 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 26 Jul 2025 09:19:10 +0200 Subject: [PATCH] [haxe] Plan for serializer generator --- tests/package-lock.json | 84 +++- tests/plan-haxe.md | 927 ++++++++++++++++++++++++++++++++++++++++ todos/todos.md | 15 +- 3 files changed, 1014 insertions(+), 12 deletions(-) create mode 100644 tests/plan-haxe.md diff --git a/tests/package-lock.json b/tests/package-lock.json index d82d340a5..efec99714 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -20,6 +20,7 @@ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz", "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==", "dev": true, + "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" }, @@ -49,6 +50,7 @@ "arm64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "darwin" @@ -65,6 +67,7 @@ "x64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "darwin" @@ -81,6 +84,7 @@ "arm64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "linux" @@ -97,6 +101,7 @@ "arm64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "linux" @@ -113,6 +118,7 @@ "x64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "linux" @@ -129,6 +135,7 @@ "x64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "linux" @@ -145,6 +152,7 @@ "arm64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "win32" @@ -161,6 +169,7 @@ "x64" ], "dev": true, + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "win32" @@ -177,6 +186,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -193,6 +203,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -209,6 +220,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -225,6 +237,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -241,6 +254,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -257,6 +271,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -273,6 +288,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -289,6 +305,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -305,6 +322,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -321,6 +339,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -337,6 +356,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -353,6 +373,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -369,6 +390,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -385,6 +407,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -401,6 +424,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -417,6 +441,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -433,6 +458,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -449,6 +475,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -465,6 +492,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -481,6 +509,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -497,6 +526,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -513,6 +543,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -529,6 +560,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -545,6 +577,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -561,6 +594,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -577,6 +611,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -606,6 +641,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -614,6 +650,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -625,6 +662,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", "engines": { "node": ">=10" } @@ -633,6 +671,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -641,13 +680,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz", "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^2.19.0", "lru-cache": "^4.1.5", @@ -662,7 +703,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { "version": "0.25.8", @@ -670,6 +712,7 @@ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -709,6 +752,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -720,6 +764,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -733,6 +778,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -746,6 +792,7 @@ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -758,6 +805,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, + "license": "ISC", "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -767,12 +815,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", "engines": { "node": ">=8" } @@ -781,6 +831,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -793,6 +844,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -804,6 +856,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -815,6 +868,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "license": "MIT", "engines": { "node": ">=0.12.0" }, @@ -827,13 +881,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -843,6 +899,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -851,12 +908,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -874,6 +933,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -893,6 +953,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", @@ -907,6 +968,7 @@ "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz", "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==", "dev": true, + "license": "MIT", "dependencies": { "commandpost": "^1.0.0", "editorconfig": "^0.15.0" @@ -925,12 +987,14 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vscode-jsonrpc": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -939,6 +1003,7 @@ "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" @@ -948,6 +1013,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -955,12 +1021,14 @@ "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/tests/plan-haxe.md b/tests/plan-haxe.md new file mode 100644 index 000000000..bb326157d --- /dev/null +++ b/tests/plan-haxe.md @@ -0,0 +1,927 @@ +# Haxe Serializer Generator Implementation Plan + +## Overview + +This document outlines the complete implementation plan for adding Haxe support to the Spine runtime testing infrastructure. The goal is to generate a Haxe serializer that produces identical JSON output to the existing Java and C++ serializers, enabling cross-runtime compatibility testing. + +## Current System Architecture + +The existing system consists of three layers: + +1. **SerializerIR Generation** (`tests/src/generate-serializer-ir.ts`) + - Analyzes Java API to create intermediate representation + - Outputs `tests/output/serializer-ir.json` with type and property metadata + +2. **Language-Specific Generators** + - `tests/src/generate-java-serializer.ts` - Java implementation + - `tests/src/generate-cpp-serializer.ts` - C++ implementation + - **Missing**: `tests/src/generate-haxe-serializer.ts` + +3. **HeadlessTest Applications** + - `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java` + - `spine-cpp/tests/HeadlessTest.cpp` + - **Missing**: `spine-haxe/tests/HeadlessTest.hx` + +4. **Test Runner** (`tests/src/headless-test-runner.ts`) + - Orchestrates building and running tests + - Compares outputs for consistency + - Currently supports: Java, C++ + - **Needs**: Haxe support + +## SerializerIR Structure Reference + +Based on `tests/src/generate-serializer-ir.ts:10-80`: + +```typescript +interface SerializerIR { + publicMethods: PublicMethod[]; // Entry point methods + writeMethods: WriteMethod[]; // Type-specific serializers + enumMappings: { [enumName: string]: { [javaValue: string]: string } }; +} + +interface WriteMethod { + name: string; // writeSkeletonData, writeBone, etc. + paramType: string; // Full Java class name + properties: Property[]; // Fields to serialize + isAbstractType: boolean; // Needs instanceof chain + subtypeChecks?: SubtypeCheck[]; // For abstract types +} + +type Property = Primitive | Object | Enum | Array | NestedArray; +``` + +## Implementation Plan + +### 1. Generate Haxe Serializer (`tests/src/generate-haxe-serializer.ts`) + +Create the generator following the pattern from existing generators: + +```typescript +#!/usr/bin/env tsx + +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; +import type { Property, SerializerIR } from './generate-serializer-ir'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function transformType(javaType: string): string { + // Java → Haxe type mappings + const primitiveMap: Record = { + 'String': 'String', + 'int': 'Int', + 'float': 'Float', + 'boolean': 'Bool', + 'short': 'Int', + 'byte': 'Int', + 'double': 'Float', + 'long': 'Int' + }; + + // Remove package prefixes and map primitives + const simpleName = javaType.includes('.') ? javaType.split('.').pop()! : javaType; + + if (primitiveMap[simpleName]) { + return primitiveMap[simpleName]; + } + + // Handle arrays: Java T[] → Haxe Array + if (simpleName.endsWith('[]')) { + const baseType = simpleName.slice(0, -2); + return `Array<${transformType(baseType)}>`; + } + + // Java Array stays Array in Haxe + if (simpleName.startsWith('Array<')) { + return simpleName; + } + + // Object types: keep class name, remove package + return simpleName; +} + +function mapJavaGetterToHaxeField(javaGetter: string, objName: string): string { + // Map Java getter methods to Haxe field access + // Based on analysis of existing Haxe classes in spine-haxe/spine-haxe/spine/ + + if (javaGetter.endsWith('()')) { + const methodName = javaGetter.slice(0, -2); + + // Remove get/is prefix and convert to camelCase field + if (methodName.startsWith('get')) { + const fieldName = methodName.slice(3); + const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1); + return `${objName}.${haxeField}`; + } + + if (methodName.startsWith('is')) { + const fieldName = methodName.slice(2); + const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1); + return `${objName}.${haxeField}`; + } + + // Some methods might be direct field names + return `${objName}.${methodName}`; + } + + // Direct field access (already in correct format) + return `${objName}.${javaGetter}`; +} + +function generatePropertyCode(property: Property, indent: string, enumMappings: { [enumName: string]: { [javaValue: string]: string } }): string[] { + const lines: string[] = []; + const accessor = mapJavaGetterToHaxeField(property.getter, 'obj'); + + switch (property.kind) { + case "primitive": + lines.push(`${indent}json.writeValue(${accessor});`); + break; + + case "object": + if (property.isNullable) { + lines.push(`${indent}if (${accessor} == null) {`); + lines.push(`${indent} json.writeNull();`); + lines.push(`${indent}} else {`); + lines.push(`${indent} ${property.writeMethodCall}(${accessor});`); + lines.push(`${indent}}`); + } else { + lines.push(`${indent}${property.writeMethodCall}(${accessor});`); + } + break; + + case "enum": { + const enumName = property.enumName; + const enumMap = enumMappings[enumName]; + + if (property.isNullable) { + lines.push(`${indent}if (${accessor} == null) {`); + lines.push(`${indent} json.writeNull();`); + lines.push(`${indent}} else {`); + } + + if (enumMap && Object.keys(enumMap).length > 0) { + // Generate switch statement for enum mapping + lines.push(`${indent}${property.isNullable ? ' ' : ''}switch (${accessor}) {`); + + for (const [javaValue, haxeValue] of Object.entries(enumMap)) { + lines.push(`${indent}${property.isNullable ? ' ' : ''} case ${haxeValue}: json.writeValue("${javaValue}");`); + } + + lines.push(`${indent}${property.isNullable ? ' ' : ''} default: json.writeValue("unknown");`); + lines.push(`${indent}${property.isNullable ? ' ' : ''}}`); + } else { + // Fallback using Type.enumConstructor or similar + lines.push(`${indent}${property.isNullable ? ' ' : ''}json.writeValue(Type.enumConstructor(${accessor}));`); + } + + if (property.isNullable) { + lines.push(`${indent}}`); + } + break; + } + + case "array": { + if (property.isNullable) { + lines.push(`${indent}if (${accessor} == null) {`); + lines.push(`${indent} json.writeNull();`); + lines.push(`${indent}} else {`); + lines.push(`${indent} json.writeArrayStart();`); + lines.push(`${indent} for (item in ${accessor}) {`); + } else { + lines.push(`${indent}json.writeArrayStart();`); + lines.push(`${indent}for (item in ${accessor}) {`); + } + + const itemIndent = property.isNullable ? `${indent} ` : `${indent} `; + if (property.elementKind === "primitive") { + lines.push(`${itemIndent}json.writeValue(item);`); + } else { + lines.push(`${itemIndent}${property.writeMethodCall}(item);`); + } + + if (property.isNullable) { + lines.push(`${indent} }`); + lines.push(`${indent} json.writeArrayEnd();`); + lines.push(`${indent}}`); + } else { + lines.push(`${indent}}`); + lines.push(`${indent}json.writeArrayEnd();`); + } + break; + } + + case "nestedArray": { + if (property.isNullable) { + lines.push(`${indent}if (${accessor} == null) {`); + lines.push(`${indent} json.writeNull();`); + lines.push(`${indent}} else {`); + } + + const outerIndent = property.isNullable ? `${indent} ` : indent; + lines.push(`${outerIndent}json.writeArrayStart();`); + lines.push(`${outerIndent}for (nestedArray in ${accessor}) {`); + lines.push(`${outerIndent} if (nestedArray == null) {`); + lines.push(`${outerIndent} json.writeNull();`); + lines.push(`${outerIndent} } else {`); + lines.push(`${outerIndent} json.writeArrayStart();`); + lines.push(`${outerIndent} for (elem in nestedArray) {`); + lines.push(`${outerIndent} json.writeValue(elem);`); + lines.push(`${outerIndent} }`); + lines.push(`${outerIndent} json.writeArrayEnd();`); + lines.push(`${outerIndent} }`); + lines.push(`${outerIndent}}`); + lines.push(`${outerIndent}json.writeArrayEnd();`); + + if (property.isNullable) { + lines.push(`${indent}}`); + } + break; + } + } + + return lines; +} + +function generateHaxeFromIR(ir: SerializerIR): string { + const haxeOutput: string[] = []; + + // Generate Haxe file header + haxeOutput.push('package spine.utils;'); + haxeOutput.push(''); + haxeOutput.push('import haxe.ds.StringMap;'); + haxeOutput.push('import spine.*;'); + haxeOutput.push('import spine.animation.*;'); + haxeOutput.push('import spine.attachments.*;'); + haxeOutput.push(''); + haxeOutput.push('class SkeletonSerializer {'); + haxeOutput.push(' private var visitedObjects:StringMap = new StringMap();'); + haxeOutput.push(' private var nextId:Int = 1;'); + haxeOutput.push(' private var json:JsonWriter;'); + haxeOutput.push(''); + haxeOutput.push(' public function new() {}'); + haxeOutput.push(''); + + // Generate public methods + for (const method of ir.publicMethods) { + const haxeParamType = transformType(method.paramType); + haxeOutput.push(` public function ${method.name}(${method.paramName}:${haxeParamType}):String {`); + haxeOutput.push(' visitedObjects = new StringMap();'); + haxeOutput.push(' nextId = 1;'); + haxeOutput.push(' json = new JsonWriter();'); + haxeOutput.push(` ${method.writeMethodCall}(${method.paramName});`); + haxeOutput.push(' return json.getString();'); + haxeOutput.push(' }'); + haxeOutput.push(''); + } + + // Generate write methods + for (const method of ir.writeMethods) { + const shortName = method.paramType.split('.').pop(); + const haxeType = transformType(method.paramType); + + haxeOutput.push(` private function ${method.name}(obj:${haxeType}):Void {`); + + if (method.isAbstractType) { + // Handle abstract types with Std.isOfType chain (Haxe equivalent of instanceof) + if (method.subtypeChecks && method.subtypeChecks.length > 0) { + let first = true; + for (const subtype of method.subtypeChecks) { + const subtypeHaxeName = transformType(subtype.typeName); + + if (first) { + haxeOutput.push(` if (Std.isOfType(obj, ${subtypeHaxeName})) {`); + first = false; + } else { + haxeOutput.push(` } else if (Std.isOfType(obj, ${subtypeHaxeName})) {`); + } + haxeOutput.push(` ${subtype.writeMethodCall}(cast(obj, ${subtypeHaxeName}));`); + } + haxeOutput.push(' } else {'); + haxeOutput.push(` throw new spine.SpineException("Unknown ${shortName} type");`); + haxeOutput.push(' }'); + } else { + haxeOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions'); + } + } else { + // Handle concrete types - add cycle detection + haxeOutput.push(' if (visitedObjects.exists(obj)) {'); + haxeOutput.push(' json.writeValue(visitedObjects.get(obj));'); + haxeOutput.push(' return;'); + haxeOutput.push(' }'); + + // Generate reference string + const nameGetter = method.properties.find(p => + (p.kind === 'object' || p.kind === "primitive") && + p.getter === 'getName()' && + p.valueType === 'String' + ); + + if (nameGetter) { + const nameAccessor = mapJavaGetterToHaxeField('getName()', 'obj'); + haxeOutput.push(` var refString = ${nameAccessor} != null ? "<${shortName}-" + ${nameAccessor} + ">" : "<${shortName}-" + (nextId++) + ">";`); + } else { + haxeOutput.push(` var refString = "<${shortName}-" + (nextId++) + ">";`); + } + haxeOutput.push(' visitedObjects.set(obj, refString);'); + haxeOutput.push(''); + + haxeOutput.push(' json.writeObjectStart();'); + + // Write reference string and type + haxeOutput.push(' json.writeName("refString");'); + haxeOutput.push(' json.writeValue(refString);'); + haxeOutput.push(' json.writeName("type");'); + haxeOutput.push(` json.writeValue("${shortName}");`); + + // Write properties + for (const property of method.properties) { + haxeOutput.push(''); + haxeOutput.push(` json.writeName("${property.name}");`); + const propertyLines = generatePropertyCode(property, ' ', ir.enumMappings); + haxeOutput.push(...propertyLines); + } + + haxeOutput.push(''); + haxeOutput.push(' json.writeObjectEnd();'); + } + + haxeOutput.push(' }'); + haxeOutput.push(''); + } + + // Add helper methods for special types (following C++ pattern) + haxeOutput.push(' // Helper methods for special types'); + haxeOutput.push(' private function writeColor(obj:spine.Color):Void {'); + haxeOutput.push(' if (obj == null) {'); + haxeOutput.push(' json.writeNull();'); + haxeOutput.push(' } else {'); + haxeOutput.push(' json.writeObjectStart();'); + haxeOutput.push(' json.writeName("r");'); + haxeOutput.push(' json.writeValue(obj.r);'); + haxeOutput.push(' json.writeName("g");'); + haxeOutput.push(' json.writeValue(obj.g);'); + haxeOutput.push(' json.writeName("b");'); + haxeOutput.push(' json.writeValue(obj.b);'); + haxeOutput.push(' json.writeName("a");'); + haxeOutput.push(' json.writeValue(obj.a);'); + haxeOutput.push(' json.writeObjectEnd();'); + haxeOutput.push(' }'); + haxeOutput.push(' }'); + haxeOutput.push(''); + + haxeOutput.push('}'); + + return haxeOutput.join('\n'); +} + +async function main(): Promise { + try { + // Read the IR file + const irFile = path.resolve(__dirname, '../output/serializer-ir.json'); + if (!fs.existsSync(irFile)) { + console.error('Serializer IR not found. Run generate-serializer-ir.ts first.'); + process.exit(1); + } + + const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8')); + + // Generate Haxe serializer from IR + const haxeCode = generateHaxeFromIR(ir); + + // Write the Haxe file + const haxeFile = path.resolve( + __dirname, + '../../spine-haxe/spine-haxe/spine/utils/SkeletonSerializer.hx' + ); + + fs.mkdirSync(path.dirname(haxeFile), { recursive: true }); + fs.writeFileSync(haxeFile, haxeCode); + + console.log(`Generated Haxe serializer from IR: ${haxeFile}`); + console.log(`- ${ir.publicMethods.length} public methods`); + console.log(`- ${ir.writeMethods.length} write methods`); + console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`); + + } catch (error: any) { + console.error('Error:', error.message); + console.error('Stack:', error.stack); + process.exit(1); + } +} + +// Allow running as a script or importing the function +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { generateHaxeFromIR }; +``` + +### 2. JsonWriter Helper Class (`spine-haxe/spine-haxe/spine/utils/JsonWriter.hx`) + +Based on the pattern from `spine-cpp/tests/JsonWriter.h`, create a Haxe equivalent: + +```haxe +package spine.utils; + +enum JsonContext { + Object; + Array; +} + +class JsonWriter { + private var buffer:StringBuf = new StringBuf(); + private var needsComma:Bool = false; + private var contexts:Array = []; + + public function new() { + buffer = new StringBuf(); + needsComma = false; + contexts = []; + } + + public function writeObjectStart():Void { + writeCommaIfNeeded(); + buffer.add("{"); + contexts.push(Object); + needsComma = false; + } + + public function writeObjectEnd():Void { + buffer.add("}"); + contexts.pop(); + needsComma = true; + } + + public function writeArrayStart():Void { + writeCommaIfNeeded(); + buffer.add("["); + contexts.push(Array); + needsComma = false; + } + + public function writeArrayEnd():Void { + buffer.add("]"); + contexts.pop(); + needsComma = true; + } + + public function writeName(name:String):Void { + writeCommaIfNeeded(); + buffer.add('"${escapeString(name)}":'); + needsComma = false; + } + + public function writeValue(value:Dynamic):Void { + writeCommaIfNeeded(); + + if (value == null) { + buffer.add("null"); + } else if (Std.isOfType(value, String)) { + buffer.add('"${escapeString(cast(value, String))}"'); + } else if (Std.isOfType(value, Bool)) { + buffer.add(value ? "true" : "false"); + } else if (Std.isOfType(value, Float) || Std.isOfType(value, Int)) { + // Ensure consistent float formatting (C locale style) + buffer.add(Std.string(value)); + } else { + buffer.add(Std.string(value)); + } + + needsComma = true; + } + + public function writeNull():Void { + writeCommaIfNeeded(); + buffer.add("null"); + needsComma = true; + } + + public function getString():String { + return buffer.toString(); + } + + private function writeCommaIfNeeded():Void { + if (needsComma) { + buffer.add(","); + } + } + + private function escapeString(str:String):String { + // Escape special characters for JSON + str = StringTools.replace(str, "\\", "\\\\"); + str = StringTools.replace(str, '"', '\\"'); + str = StringTools.replace(str, "\n", "\\n"); + str = StringTools.replace(str, "\r", "\\r"); + str = StringTools.replace(str, "\t", "\\t"); + return str; + } +} +``` + +### 3. Haxe HeadlessTest Application (`spine-haxe/tests/HeadlessTest.hx`) + +Following the pattern from existing HeadlessTest implementations: + +```haxe +package; + +import spine.*; +import spine.atlas.TextureAtlas; +import spine.atlas.TextureAtlasPage; +import spine.atlas.TextureLoader; +import spine.attachments.AtlasAttachmentLoader; +import spine.animation.*; +import spine.utils.SkeletonSerializer; +import sys.io.File; +import haxe.io.Bytes; + +// Mock texture loader that doesn't require actual texture loading +class MockTextureLoader implements TextureLoader { + public function new() {} + + public function load(page:TextureAtlasPage, path:String):Void { + // Set mock dimensions - no actual texture loading needed + page.width = 1024; + page.height = 1024; + page.texture = {}; // Empty object as mock texture + } + + public function unload(texture:Dynamic):Void { + // Nothing to unload in headless mode + } +} + +class HeadlessTest { + static function main():Void { + var args = Sys.args(); + + if (args.length < 2) { + Sys.stderr().writeString("Usage: HeadlessTest [animation-name]\n"); + Sys.exit(1); + } + + var skeletonPath = args[0]; + var atlasPath = args[1]; + var animationName = args.length >= 3 ? args[2] : null; + + try { + // Load atlas with mock texture loader + var textureLoader = new MockTextureLoader(); + var atlasContent = File.getContent(atlasPath); + var atlas = new TextureAtlas(atlasContent, textureLoader); + + // Load skeleton data + var skeletonData:SkeletonData; + var attachmentLoader = new AtlasAttachmentLoader(atlas); + + if (StringTools.endsWith(skeletonPath, ".json")) { + var loader = new SkeletonJson(attachmentLoader); + var jsonContent = File.getContent(skeletonPath); + skeletonData = loader.readSkeletonData(jsonContent); + } else { + var loader = new SkeletonBinary(attachmentLoader); + var binaryContent = File.getBytes(skeletonPath); + skeletonData = loader.readSkeletonData(binaryContent); + } + + // Create serializer + var serializer = new SkeletonSerializer(); + + // Print skeleton data + Sys.println("=== SKELETON DATA ==="); + Sys.println(serializer.serializeSkeletonData(skeletonData)); + + // Create skeleton instance + var skeleton = new Skeleton(skeletonData); + + // Handle animation if provided + var state:AnimationState = null; + if (animationName != null) { + var stateData = new AnimationStateData(skeletonData); + state = new AnimationState(stateData); + + var animation = skeletonData.findAnimation(animationName); + if (animation == null) { + Sys.stderr().writeString('Animation not found: $animationName\n'); + Sys.exit(1); + } + + state.setAnimation(0, animation, true); + state.update(0.016); + state.apply(skeleton); + } + + // Update world transforms (following the pattern from other HeadlessTests) + skeleton.updateWorldTransform(Physics.update); + + // Print skeleton state + Sys.println("\n=== SKELETON STATE ==="); + Sys.println(serializer.serializeSkeleton(skeleton)); + + // Print animation state if present + if (state != null) { + Sys.println("\n=== ANIMATION STATE ==="); + Sys.println(serializer.serializeAnimationState(state)); + } + + } catch (e:Dynamic) { + Sys.stderr().writeString('Error: $e\n'); + Sys.exit(1); + } + } +} +``` + +### 4. Build Script (`spine-haxe/build-headless-test.sh`) + +```bash +#!/bin/bash + +# Build Haxe HeadlessTest for cross-platform execution +# Following pattern from spine-cpp/build.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "Building Haxe HeadlessTest..." + +# Clean previous build +rm -rf build/headless-test + +# Create build directory +mkdir -p build + +# Compile HeadlessTest to C++ for performance and consistency with other runtimes +haxe \ + -cp spine-haxe \ + -cp tests \ + -main HeadlessTest \ + -cpp build/headless-test \ + -D HXCPP_QUIET + +# Make executable +chmod +x build/headless-test/HeadlessTest + +echo "Build complete: build/headless-test/HeadlessTest" +``` + +### 5. Test Runner Integration (`tests/src/headless-test-runner.ts`) + +Add Haxe support to the existing test runner. Key changes needed: + +```typescript +// Line 207: Update supported languages +if (!['cpp', 'haxe'].includes(language)) { + log_detail(`Invalid target language: ${language}. Must be cpp or haxe`); + process.exit(1); +} + +// Add needsHaxeBuild function (similar to needsCppBuild at line 96) +function needsHaxeBuild(): boolean { + const haxeDir = join(SPINE_ROOT, 'spine-haxe'); + const buildDir = join(haxeDir, 'build'); + const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest'); + + try { + // Check if executable exists + if (!existsSync(headlessTest)) return true; + + // Get executable modification time + const execTime = statSync(headlessTest).mtime.getTime(); + + // Check Haxe source files + const haxeSourceTime = getNewestFileTime(join(haxeDir, 'spine-haxe'), '*.hx'); + const testSourceTime = getNewestFileTime(join(haxeDir, 'tests'), '*.hx'); + const buildScriptTime = getNewestFileTime(haxeDir, 'build-headless-test.sh'); + + const newestSourceTime = Math.max(haxeSourceTime, testSourceTime, buildScriptTime); + + return newestSourceTime > execTime; + } catch { + return true; + } +} + +// Add executeHaxe function (similar to executeCpp at line 321) +function executeHaxe(args: TestArgs): string { + const haxeDir = join(SPINE_ROOT, 'spine-haxe'); + const testsDir = join(haxeDir, 'tests'); + + if (!existsSync(testsDir)) { + log_detail(`Haxe tests directory not found: ${testsDir}`); + process.exit(1); + } + + // Check if we need to build + if (needsHaxeBuild()) { + log_action('Building Haxe HeadlessTest'); + try { + execSync('./build-headless-test.sh', { + cwd: haxeDir, + stdio: ['inherit', 'pipe', 'inherit'] + }); + log_ok(); + } catch (error: any) { + log_fail(); + log_detail(`Haxe build failed: ${error.message}`); + process.exit(1); + } + } + + // Run the headless test + const testArgs = [args.skeletonPath, args.atlasPath]; + if (args.animationName) { + testArgs.push(args.animationName); + } + + const buildDir = join(haxeDir, 'build'); + const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest'); + + if (!existsSync(headlessTest)) { + log_detail(`Haxe headless-test executable not found: ${headlessTest}`); + process.exit(1); + } + + log_action('Running Haxe HeadlessTest'); + try { + const output = execSync(`${headlessTest} ${testArgs.join(' ')}`, { + encoding: 'utf8', + maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large output + }); + log_ok(); + return output; + } catch (error: any) { + log_fail(); + log_detail(`Haxe execution failed: ${error.message}`); + process.exit(1); + } +} + +// Update runTestsForFiles function around line 525 to handle Haxe +if (language === 'cpp') { + targetOutput = executeCpp(testArgs); +} else if (language === 'haxe') { + targetOutput = executeHaxe(testArgs); +} else { + log_detail(`Unsupported target language: ${language}`); + process.exit(1); +} +``` + +### 6. Build Integration (`tests/generate-serializers.sh`) + +Update the serializer generation script to include Haxe: + +```bash +# Add after C++ generation +echo "Generating Haxe serializer..." +tsx tests/src/generate-haxe-serializer.ts + +echo "Type checking Haxe serializer..." +cd spine-haxe && haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer +cd .. +``` + +### 7. File Structure Summary + +``` +spine-haxe/ +├── spine-haxe/spine/utils/ +│ ├── SkeletonSerializer.hx (generated) +│ └── JsonWriter.hx (helper class) +├── tests/ +│ └── HeadlessTest.hx (console application) +├── build-headless-test.sh (build script) +└── build/headless-test/ (compiled executable) + └── HeadlessTest + +tests/src/ +├── generate-haxe-serializer.ts (new generator) +└── headless-test-runner.ts (updated with Haxe support) +``` + +## Type Checking and Validation + +### Compilation Validation + +Add type checking to the generator to ensure generated code compiles: + +```typescript +import { execSync } from 'child_process'; + +async function validateGeneratedHaxeCode(haxeCode: string, outputPath: string): Promise { + // Write code to file + fs.writeFileSync(outputPath, haxeCode); + + try { + // Attempt compilation without output (type check only) + execSync('haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer', { + cwd: path.resolve(__dirname, '../../spine-haxe'), + stdio: 'pipe' + }); + + console.log('✓ Generated Haxe serializer compiles successfully'); + + } catch (error: any) { + fs.unlinkSync(outputPath); + throw new Error(`Generated Haxe serializer failed to compile:\n${error.message}`); + } +} + +// Call in main() after generating code +await validateGeneratedHaxeCode(haxeCode, haxeFile); +``` + +## Key Implementation Notes + +### Java → Haxe Property Mapping + +Based on analysis of `spine-haxe/spine-haxe/spine/` classes: + +- `obj.getName()` → `obj.name` +- `obj.getBones()` → `obj.bones` +- `obj.isActive()` → `obj.active` +- `obj.getColor()` → `obj.color` + +### Enum Handling + +Haxe enums are different from Java enums. Use `Type.enumConstructor()` to get string representation: + +```haxe +// For enum serialization +json.writeValue(Type.enumConstructor(obj.blendMode)); +``` + +### Array Handling + +Haxe uses `Array` syntax similar to Java, but iteration is different: + +```haxe +// Haxe iteration +for (item in obj.bones) { + writeBone(item); +} +``` + +### Null Safety + +Haxe has explicit null checking: + +```haxe +if (obj.skin == null) { + json.writeNull(); +} else { + writeSkin(obj.skin); +} +``` + +## Testing and Verification + +### Cross-Runtime Consistency + +The test runner will automatically: + +1. Build all three runtimes (Java, C++, Haxe) +2. Run identical test cases on same skeleton files +3. Compare JSON outputs for exact matches +4. Report any differences + +### Manual Testing + +```bash +# Generate all serializers +./tests/generate-serializers.sh + +# Test specific skeleton with all runtimes +tsx tests/src/headless-test-runner.ts cpp -s spineboy idle +tsx tests/src/headless-test-runner.ts haxe -s spineboy idle + +# Compare outputs +diff tests/output/skeleton-data-cpp-json.json tests/output/skeleton-data-haxe-json.json +``` + +## Implementation Checklist + +- [ ] Create `tests/src/generate-haxe-serializer.ts` +- [ ] Create `spine-haxe/spine-haxe/spine/utils/JsonWriter.hx` +- [ ] Create `spine-haxe/tests/HeadlessTest.hx` +- [ ] Create `spine-haxe/build-headless-test.sh` +- [ ] Update `tests/src/headless-test-runner.ts` with Haxe support +- [ ] Update `tests/generate-serializers.sh` +- [ ] Test with existing skeleton examples +- [ ] Verify JSON output matches Java/C++ exactly +- [ ] Add to CI pipeline + +## Expected Benefits + +1. **Cross-Runtime Testing**: Verify Haxe runtime behavior matches Java/C++ +2. **Debugging Support**: Unified JSON format for inspection across all runtimes +3. **API Consistency**: Ensure Haxe API changes don't break compatibility +4. **Quality Assurance**: Automated verification of serialization correctness +5. **Development Velocity**: Fast detection of runtime-specific issues + +This implementation follows the established patterns while adapting to Haxe's specific language features and build system. \ No newline at end of file diff --git a/todos/todos.md b/todos/todos.md index 9fde6cc8d..6139a1f39 100644 --- a/todos/todos.md +++ b/todos/todos.md @@ -1,8 +1,15 @@ - Port C++ SkeletonRenderer and RenderCommands to all runtimes - Will be used to snapshottesting via HeadlessTest, see also tests/ - Can go into main package in all core runtimes, except for spine-libgdx, where it must go next to SkeletonSerializer in spine-libgdx-tests -- Generate language bindings in spine-c/codegen - - Use CClassOrStruct, CEnum that get generated from spine-cpp-types.json and generate - - Swift - - Dart +- Fix Dart NativeArray wrt to resize/add/remove. Current impl is wonky. Either make it read-only or support full mutabiliy (prefer latter) +- Generate bindings for Swift from spine-c generate() like dart-writer.ts +- Generate Godot wrappers from C++ types and/or spine-c generate() (unlike dart-writer.ts)? +- headless-test improvements + - should take cli args for ad-hoc testing + - if none are given, should execute a set of (regression) tests and output individual test snapshots one after the other as jsonl + - All headless tests must have the same test suite + - test runner must know how to deal with this mode +- Add serializer generator for Haxe (see tests/plan-haxe.md for a full plan) +- Add serializer generator for C# +- Add serializer generator for TypeScript - spine-c/codegen type extractor should also report typedefs like typedef long long PropertyId; so primitive type to some name, and we need to handle that in the codegen \ No newline at end of file