import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import type { CClassOrStruct, CEnum, CMethod, CParameter } from '../../../spine-c/codegen/src/c-types.js'; import { toSnakeCase } from '../../../spine-c/codegen/src/types.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LICENSE_HEADER = fs.readFileSync(path.join(__dirname, '../../../spine-cpp/src/spine/Skeleton.cpp'), 'utf8') .split('\n') .slice(0, 28) .map((line, index, array) => { // Convert C++ block comment format to Dart line comment format if (index === 0 && line.startsWith('/****')) { return `//${line.substring(4).replace(/\*+/g, '')}`; } else if (index === array.length - 1 && (line.startsWith(' ****') || line.trim() === '*/')) { return `//${line.substring(line.indexOf('*') + 1).replace(/\*+/g, '').replace(/\//g, '')}`; } else if (line.startsWith(' ****') || line.trim() === '*/') { return `// ${line.substring(4)}`; } else if (line.startsWith(' * ')) { return `// ${line.substring(3)}`; } else if (line.startsWith(' *')) { return `//${line.substring(2)}`; } else { return line; } }) .join('\n'); // Internal data model interfaces (from spec) interface DartClass { name: string; // Dart class name (e.g., "Animation") type: 'concrete' | 'abstract' | 'interface'; inheritance: { extends?: string; // Single parent class implements: string[]; // Multiple interfaces (replaces mixins) }; imports: string[]; // All required imports members: DartMember[]; // All class members hasRtti: boolean; // Whether this class needs RTTI switching needsPackageFfi: boolean; // Whether to import package:ffi } interface DartMember { type: 'constructor' | 'method' | 'getter' | 'setter' | 'static_method'; name: string; // Dart member name dartReturnType: string; // Dart return type parameters: DartParameter[]; // Parameters (excluding 'self') isOverride: boolean; // Whether to add @override implementation: string; // The actual Dart code body cMethodName?: string; // Original C method name (for reference) } interface DartParameter { name: string; dartType: string; cType: string; // Original C type for conversion } interface DartEnum { name: string; values: DartEnumValue[]; } interface DartEnumValue { name: string; value: number; } /** New Dart writer with clean architecture - following the specification exactly */ export class DartWriter { private enumNames = new Set(); private classMap = new Map(); // name -> class private inheritance: Record = {}; private isInterface: Record = {}; private supertypes: Record = {}; // for RTTI switching constructor (private outputDir: string) { this.cleanOutputDirectory(); } private cleanOutputDirectory (): void { if (fs.existsSync(this.outputDir)) { fs.rmSync(this.outputDir, { recursive: true, force: true }); } fs.mkdirSync(this.outputDir, { recursive: true }); } // Step 1: Transform C types to clean Dart model (from spec) private transformToDartModel ( cTypes: CClassOrStruct[], cEnums: CEnum[], inheritance: Record, isInterface: Record, supertypes: Record ): { classes: DartClass[], enums: DartEnum[] } { // Store data for reference this.inheritance = inheritance; this.isInterface = isInterface; this.supertypes = supertypes; for (const cType of cTypes) { this.classMap.set(cType.name, cType); } for (const cEnum of cEnums) { this.enumNames.add(cEnum.name); } const dartClasses: DartClass[] = []; const dartEnums: DartEnum[] = []; // Transform enums for (const cEnum of cEnums) { dartEnums.push(this.transformEnum(cEnum)); } // Transform classes in dependency order const sortedTypes = this.sortByInheritance(cTypes); for (const cType of sortedTypes) { dartClasses.push(this.transformClass(cType)); } return { classes: dartClasses, enums: dartEnums }; } // Step 2: Generate Dart code from clean model (from spec) private generateDartCode (dartClass: DartClass): string { const lines: string[] = []; // Header (same for all) lines.push(this.generateHeader()); // Imports (unified logic) lines.push(...this.generateImports(dartClass.imports, dartClass.hasRtti)); // Class declaration (unified) lines.push(this.generateClassDeclaration(dartClass)); // Class body (same template for all types) if (dartClass.type === 'interface') { lines.push(...this.generateInterfaceBody(dartClass)); } else { lines.push(...this.generateClassBody(dartClass)); } lines.push('}'); return lines.join('\n'); } // Step 3: Write files async writeAll ( cTypes: CClassOrStruct[], cEnums: CEnum[], cArrayTypes: CClassOrStruct[], inheritance: Record = {}, isInterface: Record = {}, supertypes: Record = {} ): Promise { // Step 1: Transform to clean model const { classes, enums } = this.transformToDartModel(cTypes, cEnums, inheritance, isInterface, supertypes); // Step 2 & 3: Generate and write files for (const dartEnum of enums) { const content = this.generateEnumCode(dartEnum); const fileName = `${toSnakeCase(dartEnum.name)}.dart`; const filePath = path.join(this.outputDir, fileName); fs.writeFileSync(filePath, content); } for (const dartClass of classes) { const content = this.generateDartCode(dartClass); const fileName = `${toSnakeCase(dartClass.name)}.dart`; const filePath = path.join(this.outputDir, fileName); fs.writeFileSync(filePath, content); } // Generate arrays.dart (crucial - this was missing!) await this.writeArraysFile(cArrayTypes); // Generate web init file with all opaque types await this.writeWebInitFile(cTypes, cArrayTypes); // Write main export file await this.writeExportFile(classes, enums); } // Class type resolution (from spec) private determineClassType (cType: CClassOrStruct): 'concrete' | 'abstract' | 'interface' { if (this.isInterface[cType.name]) return 'interface'; if (cType.cppType.isAbstract) return 'abstract'; return 'concrete'; } // Inheritance resolution - Use implements instead of mixins (from spec) private resolveInheritance (cType: CClassOrStruct): { extends?: string, implements: string[] } { const inheritanceInfo = this.inheritance[cType.name]; return { extends: inheritanceInfo?.extends ? this.toDartTypeName(inheritanceInfo.extends) : undefined, implements: (inheritanceInfo?.mixins || []).map(mixin => this.toDartTypeName(mixin)) // Convert mixins to implements }; } private transformEnum (cEnum: CEnum): DartEnum { return { name: this.toDartTypeName(cEnum.name), values: cEnum.values.map((value, index) => ({ name: this.toDartEnumValueName(value.name, cEnum.name), // C enums without explicit values are implicitly numbered 0, 1, 2, etc. value: value.value !== undefined ? Number.parseInt(value.value) : index })) }; } private transformClass (cType: CClassOrStruct): DartClass { const dartName = this.toDartTypeName(cType.name); const classType = this.determineClassType(cType); const inheritance = this.resolveInheritance(cType); return { name: dartName, type: classType, inheritance, imports: this.collectImports(cType), members: this.processMembers(cType, classType), hasRtti: this.hasRttiMethod(cType), needsPackageFfi: this.needsStringConversions(cType) }; } // Unified Method Processing (from spec) private processMembers (cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface'): DartMember[] { const members: DartMember[] = []; // Add constructors for concrete classes only if (classType === 'concrete') { for (const constr of cType.constructors) { members.push(this.createConstructor(constr, cType)); } } // Add destructor as dispose method for concrete classes if (classType === 'concrete' && cType.destructor) { members.push(this.createDisposeMethod(cType.destructor)); } // Process methods with unified logic - Apply SAME logic for ALL class types const validMethods = cType.methods.filter(method => { if (this.hasRawPointerParameters(method)) { return false; } if (this.isMethodInherited(method, cType)) { return false; } return true; }); const renumberedMethods = this.renumberMethods(validMethods, cType.name); // Create a map of overloaded setter methods for isSetter to check const overloadedSetters = this.findOverloadedSetters(renumberedMethods); for (const methodInfo of renumberedMethods) { const { method, renamedMethod } = methodInfo; members.push(this.processMethod(method, cType, classType, renamedMethod, overloadedSetters)); } return members; } private processMethod (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string, overloadedSetters?: Set): DartMember { // Apply SAME logic for ALL class types (concrete, abstract, interface) - from spec if (this.isGetter(method)) { return this.createGetter(method, cType, classType, renamedMethod); } else if (this.isSetter(method, overloadedSetters)) { return this.createSetter(method, cType, classType, renamedMethod); } else { return this.createMethod(method, cType, classType, renamedMethod); } } // Unified getter detection for ALL classes (from spec) private isGetter (method: CMethod): boolean { return (method.name.includes('_get_') && method.parameters.length === 1) || (method.returnType === 'bool' && method.parameters.length === 1 && (method.name.includes('_has_') || method.name.includes('_is_') || method.name.includes('_was_'))); } private isSetter (method: CMethod, overloadedSetters?: Set): boolean { const isBasicSetter = method.returnType === 'void' && method.parameters.length === 2 && method.name.includes('_set_'); if (!isBasicSetter) return false; // If this setter has overloads (multiple methods with same base name), // don't generate it as a setter - generate as regular method instead if (overloadedSetters?.has(method.name)) { return false; } return true; } private findOverloadedSetters (renumberedMethods: Array<{ method: CMethod, renamedMethod?: string }>): Set { const setterBasenames = new Map(); // Group setter methods by their base name for (const methodInfo of renumberedMethods) { const method = methodInfo.method; if (method.returnType === 'void' && method.parameters.length === 2 && method.name.includes('_set_')) { // Extract base name by removing numbered suffix const match = method.name.match(/^(.+?)_(\d+)$/); const baseName = match ? match[1] : method.name; if (!setterBasenames.has(baseName)) { setterBasenames.set(baseName, []); } setterBasenames.get(baseName)?.push(method.name); } } // Find setters that have multiple methods with the same base name const overloadedSetters = new Set(); for (const [_baseName, methodNames] of setterBasenames) { if (methodNames.length > 1) { // Multiple setters with same base name - mark all as overloaded for (const methodName of methodNames) { overloadedSetters.add(methodName); } } } return overloadedSetters; } private createDisposeMethod (destructor: CMethod): DartMember { const implementation = `SpineBindings.bindings.${destructor.name}(_ptr);`; return { type: 'method', name: 'dispose', dartReturnType: 'void', parameters: [], isOverride: false, implementation, cMethodName: destructor.name }; } private createConstructor (constr: CMethod, cType: CClassOrStruct): DartMember { const dartClassName = this.toDartTypeName(cType.name); const params = constr.parameters.map(p => ({ name: p.name, dartType: this.toDartParameterType(p), cType: p.cType })); const args = constr.parameters.map(p => this.convertDartToC(p.name, p)).join(', '); const implementation = `final ptr = SpineBindings.bindings.${constr.name}(${args}); return ${dartClassName}.fromPointer(ptr);`; return { type: 'constructor', name: this.getConstructorName(constr, cType), dartReturnType: dartClassName, parameters: params, isOverride: false, implementation, cMethodName: constr.name }; } private createGetter (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember { const propertyName = renamedMethod || this.extractPropertyName(method.name, cType.name); const dartReturnType = this.toDartReturnType(method.returnType, method.returnTypeNullable); // Interface methods have no implementation (from spec) let implementation = ''; if (classType !== 'interface') { implementation = `final result = SpineBindings.bindings.${method.name}(_ptr); ${this.generateReturnConversion(method.returnType, 'result', method.returnTypeNullable)}`; } // Check if this is an override const isOverride = this.isMethodOverride(method, cType); return { type: 'getter', name: propertyName, dartReturnType, parameters: [], isOverride, implementation, cMethodName: method.name }; } private createSetter (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember { let propertyName = renamedMethod || this.extractPropertyName(method.name, cType.name); const param = method.parameters[1]; // First param is self const dartParam = { name: 'value', dartType: this.toDartParameterType(param), cType: param.cType }; // Handle numeric suffixes in setter names (only when no renamed method provided) if (!renamedMethod) { const match = propertyName.match(/^(\w+)_(\d+)$/); if (match) { propertyName = `${match[1]}${match[2]}`; } else if (/^\d+$/.test(propertyName)) { propertyName = `set${propertyName}`; } } // Interface methods have no implementation (from spec) let implementation = ''; if (classType !== 'interface') { implementation = `SpineBindings.bindings.${method.name}(_ptr, ${this.convertDartToC('value', param)});`; } const isOverride = this.isMethodOverride(method, cType); return { type: 'setter', name: propertyName, dartReturnType: 'void', parameters: [dartParam], isOverride, implementation, cMethodName: method.name }; } private createMethod (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember { let methodName = renamedMethod || this.toDartMethodName(method.name, cType.name); const dartReturnType = this.toDartReturnType(method.returnType, method.returnTypeNullable); // Check if this is a static method const isStatic = method.parameters.length === 0 || (method.parameters[0].name !== 'self' && !method.parameters[0].cType.startsWith(cType.name)); // Rename static rtti method to avoid conflict with getter if (isStatic && methodName === 'rtti') { methodName = 'rttiStatic'; } // Parameters (skip 'self' parameter for instance methods) const paramStartIndex = isStatic ? 0 : 1; const params = method.parameters.slice(paramStartIndex).map(p => ({ name: p.name, dartType: this.toDartParameterType(p), cType: p.cType })); // Interface methods have no implementation (from spec) // Exception: rttiStatic() needs implementation even in interfaces let implementation = ''; if (classType !== 'interface' || methodName === 'rttiStatic') { const args = method.parameters.map((p, i) => { if (!isStatic && i === 0) return '_ptr'; // self parameter return this.convertDartToC(p.name, p); }).join(', '); if (method.returnType === 'void') { implementation = `SpineBindings.bindings.${method.name}(${args});`; } else { implementation = `final result = SpineBindings.bindings.${method.name}(${args}); ${this.generateReturnConversion(method.returnType, 'result', method.returnTypeNullable)}`; } } const isOverride = this.isMethodOverride(method, cType); return { type: isStatic ? 'static_method' : 'method', name: methodName, dartReturnType, parameters: params, isOverride, implementation, cMethodName: method.name }; } // Code generation methods (from spec) private generateHeader (): string { return `${LICENSE_HEADER} // AUTO GENERATED FILE, DO NOT EDIT.`; } private generateImports (imports: string[], hasRtti: boolean): string[] { const lines: string[] = []; lines.push(''); lines.push("import '../ffi_proxy.dart';"); lines.push("import 'spine_dart_bindings_generated.dart';"); lines.push("import '../spine_bindings.dart';"); if (hasRtti) { lines.push("import 'rtti.dart';"); } // Add other imports for (const importFile of imports.sort()) { if (!['rtti.dart'].includes(importFile)) { // Skip duplicates lines.push(`import '${importFile}';`); } } return lines; } // Class declaration generation (from spec) private generateClassDeclaration (dartClass: DartClass): string { let declaration = ''; if (dartClass.type === 'interface') { declaration = `abstract class ${dartClass.name}`; } else { declaration = `class ${dartClass.name}`; if (dartClass.type === 'abstract') { declaration = `abstract ${declaration}`; } } // Inheritance if (dartClass.inheritance.extends) { declaration += ` extends ${dartClass.inheritance.extends}`; } // Implements clause const implementsClasses: string[] = []; // Add interfaces implementsClasses.push(...dartClass.inheritance.implements); if (implementsClasses.length > 0) { declaration += ` implements ${implementsClasses.join(', ')}`; } return ` /// ${dartClass.name} wrapper ${declaration} {`; } private generateInterfaceBody (dartClass: DartClass): string[] { const lines: string[] = []; // Add nativePtr getter for interfaces that can be used as method parameters // This allows the generated code to call .nativePtr.cast() on interface instances lines.push(' Pointer get nativePtr;'); // Generate abstract method signatures for interfaces for (const member of dartClass.members) { lines.push(this.generateInterfaceMember(member)); } return lines; } // Class body generation (from spec) private generateClassBody (dartClass: DartClass): string[] { const lines: string[] = []; // Pointer field (only for concrete/abstract classes) const cTypeName = this.toCTypeName(dartClass.name); lines.push(` final Pointer<${cTypeName}_wrapper> _ptr;`); lines.push(''); // Constructor lines.push(this.generatePointerConstructor(dartClass)); lines.push(''); // Native pointer getter lines.push(' /// Get the native pointer for FFI calls'); lines.push(' Pointer get nativePtr => _ptr;'); lines.push(''); // Members for (const member of dartClass.members) { lines.push(this.generateMember(member)); lines.push(''); } return lines; } private generatePointerConstructor (dartClass: DartClass): string { if (dartClass.inheritance.extends) { return ` ${dartClass.name}.fromPointer(this._ptr) : super.fromPointer(_ptr.cast());`; } else { return ` ${dartClass.name}.fromPointer(this._ptr);`; } } private generateInterfaceMember (member: DartMember): string { const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', '); switch (member.type) { case 'getter': return ` ${member.dartReturnType} get ${member.name};`; case 'setter': return ` set ${member.name}(${params});`; case 'method': return ` ${member.dartReturnType} ${member.name}(${params});`; case 'static_method': // Special case: rttiStatic() needs implementation even in abstract classes if (member.name === 'rttiStatic') { return ` static ${member.dartReturnType} ${member.name}(${params}) { ${member.implementation} }`; } else { return ` static ${member.dartReturnType} ${member.name}(${params});`; } default: return ''; } } // Member generation (from spec) private generateMember (member: DartMember): string { const override = member.isOverride ? '@override\n ' : ' '; switch (member.type) { case 'constructor': return this.generateConstructorMember(member); case 'getter': return `${override}${member.dartReturnType} get ${member.name} { ${member.implementation} }`; case 'setter': { const param = member.parameters[0]; return `${override}set ${member.name}(${param.dartType} ${param.name}) { ${member.implementation} }`; } case 'method': case 'static_method': { const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', '); const static_ = member.type === 'static_method' ? 'static ' : ''; return `${override}${static_}${member.dartReturnType} ${member.name}(${params}) { ${member.implementation} }`; } default: return ''; } } private generateConstructorMember (member: DartMember): string { const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', '); const factoryName = member.name === member.dartReturnType ? '' : `.${member.name}`; return ` factory ${member.dartReturnType}${factoryName}(${params}) { ${member.implementation} }`; } private generateEnumCode (dartEnum: DartEnum): string { const lines: string[] = []; lines.push(this.generateHeader()); lines.push(''); lines.push(`/// ${dartEnum.name} enum`); lines.push(`enum ${dartEnum.name} {`); // Write enum values for (let i = 0; i < dartEnum.values.length; i++) { const value = dartEnum.values[i]; const comma = i < dartEnum.values.length - 1 ? ',' : ';'; lines.push(` ${value.name}(${value.value})${comma}`); } lines.push(''); lines.push(` const ${dartEnum.name}(this.value);`); lines.push(' final int value;'); lines.push(''); lines.push(` static ${dartEnum.name} fromValue(int value) {`); lines.push(' return values.firstWhere('); lines.push(' (e) => e.value == value,'); lines.push(` orElse: () => throw ArgumentError('Invalid ${dartEnum.name} value: \$value'),`); lines.push(' );'); lines.push(' }'); lines.push('}'); return lines.join('\n'); } // Generate arrays.dart file (this was missing!) private async writeArraysFile (cArrayTypes: CClassOrStruct[]): Promise { const lines: string[] = []; lines.push(this.generateHeader()); lines.push(''); lines.push("import '../ffi_proxy.dart';"); lines.push("import 'spine_dart_bindings_generated.dart';"); lines.push("import '../spine_bindings.dart';"); lines.push("import '../native_array.dart';"); // Collect all imports needed for all array types const imports = new Set(); for (const arrayType of cArrayTypes) { const elementType = this.extractArrayElementType(arrayType.name); if (!this.isPrimitiveArrayType(elementType)) { imports.add(`import '${toSnakeCase(elementType)}.dart';`); // If this element type is abstract, we need to import all its concrete subclasses too const cElementType = `spine_${toSnakeCase(elementType)}`; const cClass = this.classMap.get(cElementType); if (cClass && this.isAbstract(cClass)) { const concreteSubclasses = this.getConcreteSubclasses(cElementType); for (const subclass of concreteSubclasses) { const dartSubclass = this.toDartTypeName(subclass); imports.add(`import '${toSnakeCase(dartSubclass)}.dart';`); } } } } // Add RTTI import if needed if (Array.from(imports).some(imp => { const arrayType = cArrayTypes.find(at => imp.includes(toSnakeCase(this.extractArrayElementType(at.name)))); if (arrayType) { const elementType = this.extractArrayElementType(arrayType.name); const cElementType = `spine_${toSnakeCase(elementType)}`; const cClass = this.classMap.get(cElementType); return cClass && this.isAbstract(cClass); } return false; })) { lines.push("import 'rtti.dart';"); } // Add sorted imports for (const imp of Array.from(imports).sort()) { lines.push(imp); } // Generate all array classes in one file (from spec) for (const arrayType of cArrayTypes) { lines.push(''); lines.push(...this.generateArrayClassLines(arrayType)); } const filePath = path.join(this.outputDir, 'arrays.dart'); fs.writeFileSync(filePath, lines.join('\n')); } // Array generation (proper implementation from old writer) private generateArrayClassLines (arrayType: CClassOrStruct): string[] { const lines: string[] = []; const dartClassName = this.toDartTypeName(arrayType.name); const elementType = this.extractArrayElementType(arrayType.name); lines.push(`/// ${dartClassName} wrapper`); lines.push(`class ${dartClassName} extends NativeArray<${this.toDartElementType(elementType)}> {`); lines.push(' final bool _ownsMemory;'); lines.push(''); // Generate typed constructor - arrays use the array wrapper type const arrayWrapperType = `${arrayType.name}_wrapper`; lines.push(` ${dartClassName}.fromPointer(Pointer<${arrayWrapperType}> ptr, {bool ownsMemory = false}) : _ownsMemory = ownsMemory, super(ptr);`); lines.push(''); // Find create methods for constructors const createMethod = arrayType.constructors?.find(m => m.name === `${arrayType.name}_create`); const createWithCapacityMethod = arrayType.constructors?.find(m => m.name === `${arrayType.name}_create_with_capacity`); // Add default constructor if (createMethod) { lines.push(' /// Create a new empty array'); lines.push(` factory ${dartClassName}() {`); lines.push(` final ptr = SpineBindings.bindings.${createMethod.name}();`); lines.push(` return ${dartClassName}.fromPointer(ptr.cast(), ownsMemory: true);`); lines.push(' }'); lines.push(''); } // Add constructor with initial capacity if (createWithCapacityMethod) { lines.push(' /// Create a new array with the specified initial capacity'); lines.push(` factory ${dartClassName}.withCapacity(int initialCapacity) {`); lines.push(` final ptr = SpineBindings.bindings.${createWithCapacityMethod.name}(initialCapacity);`); lines.push(` return ${dartClassName}.fromPointer(ptr.cast(), ownsMemory: true);`); lines.push(' }'); lines.push(''); } // Find size and buffer methods const sizeMethod = arrayType.methods.find(m => m.name.endsWith('_size') && !m.name.endsWith('_set_size')); const bufferMethod = arrayType.methods.find(m => m.name.endsWith('_buffer')); const setMethod = arrayType.methods.find(m => m.name.endsWith('_set') && m.parameters.length === 3); // self, index, value const setSizeMethod = arrayType.methods.find(m => m.name.endsWith('_set_size')); const addMethod = arrayType.methods.find(m => m.name.endsWith('_add') && !m.name.endsWith('_add_all')); const clearMethod = arrayType.methods.find(m => m.name.endsWith('_clear') && !m.name.endsWith('_clear_and_add_all')); const removeAtMethod = arrayType.methods.find(m => m.name.endsWith('_remove_at')); const ensureCapacityMethod = arrayType.methods.find(m => m.name.endsWith('_ensure_capacity')); if (sizeMethod) { lines.push(' @override'); lines.push(' int get length {'); lines.push(` return SpineBindings.bindings.${sizeMethod.name}(nativePtr.cast());`); lines.push(' }'); lines.push(''); } if (bufferMethod) { lines.push(' @override'); lines.push(` ${this.toDartElementType(elementType)} operator [](int index) {`); lines.push(' if (index < 0 || index >= length) {'); lines.push(' throw RangeError.index(index, this, \'index\');'); lines.push(' }'); lines.push(` final buffer = SpineBindings.bindings.${bufferMethod.name}(nativePtr.cast());`); // Handle different element types if (elementType === 'int') { lines.push(' return buffer.cast()[index];'); } else if (elementType === 'float') { lines.push(' return buffer.cast()[index];'); } else if (elementType === 'bool') { lines.push(' return buffer.cast()[index] != 0;'); } else if (elementType === 'unsigned_short') { lines.push(' return buffer.cast()[index];'); } else if (elementType === 'property_id') { // PropertyId buffer returns int instead of Pointer due to C codegen bug lines.push(' // NOTE: This will not compile due to C API bug - buffer() returns int instead of Pointer'); lines.push(' return buffer.cast()[index];'); } else { // For object types, the buffer contains pointers const dartElementType = this.toDartTypeName(`spine_${toSnakeCase(elementType)}`); const cElementType = `spine_${toSnakeCase(elementType)}`; const cClass = this.classMap.get(cElementType); if (cClass && this.isAbstract(cClass)) { // Use RTTI to determine concrete type for abstract classes - handle null case lines.push(` if (buffer[index].address == 0) return null;`); const rttiCode = this.generateRttiBasedInstantiation(dartElementType, 'buffer[index]', cClass); lines.push(` ${rttiCode}`); } else { // For array elements, check if the pointer is null lines.push(` return buffer[index].address == 0 ? null : ${dartElementType}.fromPointer(buffer[index]);`); } } lines.push(' }'); lines.push(''); } // Override []= if there's a set method if (setMethod) { lines.push(' @override'); lines.push(` void operator []=(int index, ${this.toDartElementType(elementType)} value) {`); lines.push(' if (index < 0 || index >= length) {'); lines.push(' throw RangeError.index(index, this, \'index\');'); lines.push(' }'); // Convert value to C type const param = setMethod.parameters[2]; // The value parameter // Create a copy of the parameter with nullable flag for proper conversion const nullableParam = { ...param, isNullable: !this.isPrimitiveArrayType(elementType) }; const convertedValue = this.convertDartToC('value', nullableParam); lines.push(` SpineBindings.bindings.${setMethod.name}(nativePtr.cast(), index, ${convertedValue});`); lines.push(' }'); lines.push(''); } // Override set length if there's a set_size method if (setSizeMethod) { lines.push(' @override'); lines.push(' set length(int newLength) {'); // For primitive types, set_size takes a default value if (this.isPrimitiveArrayType(elementType)) { let defaultValue = '0'; if (elementType === 'float') defaultValue = '0.0'; else if (elementType === 'bool') defaultValue = 'false'; lines.push(` SpineBindings.bindings.${setSizeMethod.name}(nativePtr.cast(), newLength, ${defaultValue});`); } else { // For object types, set_size takes null lines.push(` SpineBindings.bindings.${setSizeMethod.name}(nativePtr.cast(), newLength, Pointer.fromAddress(0));`); } lines.push(' }'); lines.push(''); } // Add method if available if (addMethod) { lines.push(' /// Adds a value to the end of this array.'); lines.push(` void add(${this.toDartElementType(elementType)} value) {`); // Convert value to C type const param = addMethod.parameters[1]; // The value parameter const nullableParam = { ...param, isNullable: !this.isPrimitiveArrayType(elementType) }; const convertedValue = this.convertDartToC('value', nullableParam); lines.push(` SpineBindings.bindings.${addMethod.name}(nativePtr.cast(), ${convertedValue});`); lines.push(' }'); lines.push(''); } // Clear method if available if (clearMethod) { lines.push(' /// Removes all elements from this array.'); lines.push(' @override'); lines.push(' void clear() {'); lines.push(` SpineBindings.bindings.${clearMethod.name}(nativePtr.cast());`); lines.push(' }'); lines.push(''); } // RemoveAt method if available if (removeAtMethod) { lines.push(' /// Removes the element at the given index.'); lines.push(' @override'); lines.push(` ${this.toDartElementType(elementType)} removeAt(int index) {`); lines.push(' if (index < 0 || index >= length) {'); lines.push(' throw RangeError.index(index, this, \'index\');'); lines.push(' }'); lines.push(` final value = this[index];`); lines.push(` SpineBindings.bindings.${removeAtMethod.name}(nativePtr.cast(), index);`); lines.push(' return value;'); lines.push(' }'); lines.push(''); } // EnsureCapacity method if available if (ensureCapacityMethod) { lines.push(' /// Ensures this array has at least the given capacity.'); lines.push(' void ensureCapacity(int capacity) {'); lines.push(` SpineBindings.bindings.${ensureCapacityMethod.name}(nativePtr.cast(), capacity);`); lines.push(' }'); lines.push(''); } // Find dispose method for arrays - check in destructor if (arrayType.destructor) { lines.push(' /// Dispose of the native array'); lines.push(' /// Throws an error if the array was not created by this class (i.e., it was obtained from C)'); lines.push(' void dispose() {'); lines.push(' if (!_ownsMemory) {'); lines.push(` throw StateError('Cannot dispose ${dartClassName} that was created from C. Only arrays created via factory constructors can be disposed.');`); lines.push(' }'); lines.push(` SpineBindings.bindings.${arrayType.destructor.name}(nativePtr.cast());`); lines.push(' }'); } lines.push('}'); return lines; } private extractArrayElementType (arrayTypeName: string): string { // spine_array_animation -> animation // spine_array_int -> int const match = arrayTypeName.match(/spine_array_(.+)/); if (match) { const rawType = match[1]; // For primitive types, return the raw type if (['int', 'float', 'bool', 'unsigned_short', 'property_id'].includes(rawType)) { return rawType; } // For object types, return the raw type (will be converted later) return rawType; } return 'dynamic'; } private toDartElementType (elementType: string): string { // Handle pointer types if (elementType.endsWith('*')) { const baseType = elementType.slice(0, -1).trim(); return `${this.toDartTypeName(`spine_${toSnakeCase(baseType)}`)}?`; } // For primitive types, return the Dart type directly if (elementType === 'int' || elementType === 'int32_t' || elementType === 'uint32_t' || elementType === 'size_t') { return 'int'; } if (elementType === 'unsigned_short') { return 'int'; // Dart doesn't have unsigned short, use int } if (elementType === 'property_id' || elementType === 'int64_t') { return 'int'; // PropertyId is int64_t which maps to int in Dart } if (elementType === 'float' || elementType === 'double') { return 'double'; } if (elementType === 'bool') { return 'bool'; } // For object types, convert to PascalCase and make nullable since arrays can contain null pointers return `${this.toPascalCase(elementType)}?`; } private isPrimitiveArrayType (elementType: string): boolean { return ['int', 'float', 'bool', 'unsigned_short', 'property_id'].includes(elementType.toLowerCase()); } // Helper methods private sortByInheritance (cTypes: CClassOrStruct[]): CClassOrStruct[] { const sorted: CClassOrStruct[] = []; const processed = new Set(); const processClass = (cType: CClassOrStruct) => { if (processed.has(cType.name)) { return; } // Process concrete parent first (skip interfaces) const inheritanceInfo = this.inheritance[cType.name]; if (inheritanceInfo?.extends) { const parent = this.classMap.get(inheritanceInfo.extends); if (parent) { processClass(parent); } } // Process interface dependencies for (const interfaceName of inheritanceInfo?.mixins || []) { const interfaceClass = this.classMap.get(interfaceName); if (interfaceClass) { processClass(interfaceClass); } } sorted.push(cType); processed.add(cType.name); }; for (const cType of cTypes) { processClass(cType); } return sorted; } private hasRttiMethod (cType: CClassOrStruct): boolean { return cType.methods.some(m => m.name === `${cType.name}_rtti` && m.parameters.length === 0); } private collectImports (cType: CClassOrStruct): string[] { const imports = new Set(); const currentTypeName = this.toDartTypeName(cType.name); const currentFileName = `${toSnakeCase(currentTypeName)}.dart`; // Add parent class import if needed const parentName = this.inheritance[cType.name]?.extends; if (parentName) { const parentDartName = this.toDartTypeName(parentName); imports.add(`${toSnakeCase(parentDartName)}.dart`); } // Add interface imports for (const interfaceName of this.inheritance[cType.name]?.mixins || []) { const interfaceDartName = this.toDartTypeName(interfaceName); const interfaceFileName = `${toSnakeCase(interfaceDartName)}.dart`; if (interfaceFileName !== currentFileName) { imports.add(interfaceFileName); } } // Collect from methods and constructors let hasArrays = false; for (const method of [...cType.methods, ...cType.constructors]) { if (this.hasRawPointerParameters(method)) continue; // Return type if (method.returnType.startsWith('spine_array_')) { hasArrays = true; } else if (method.returnType.startsWith('spine_')) { const cleanType = method.returnType.replace('*', '').trim(); if (!this.isPrimitive(cleanType)) { const typeName = this.toDartTypeName(cleanType); const fileName = `${toSnakeCase(typeName)}.dart`; if (fileName !== currentFileName) { imports.add(fileName); } // If return type is abstract, add imports for all concrete subclasses // that could be referenced in the RTTI-based switch statement const cClass = this.classMap.get(cleanType); if (cClass && this.isAbstract(cClass)) { const concreteSubclasses = this.getConcreteSubclasses(cleanType); for (const subclass of concreteSubclasses) { const dartSubclass = this.toDartTypeName(subclass); const subclassFileName = `${toSnakeCase(dartSubclass)}.dart`; if (subclassFileName !== currentFileName) { imports.add(subclassFileName); } } } } } // Parameters for (const param of method.parameters) { if (param.name === 'self') continue; if (param.cType.startsWith('spine_array_')) { hasArrays = true; } else if (this.enumNames.has(param.cType)) { const enumType = this.toDartTypeName(param.cType); imports.add(`${toSnakeCase(enumType)}.dart`); } else if (param.cType.startsWith('spine_')) { const cleanType = param.cType.replace('*', '').trim(); if (!this.isPrimitive(cleanType)) { const typeName = this.toDartTypeName(cleanType); const fileName = `${toSnakeCase(typeName)}.dart`; if (fileName !== currentFileName) { imports.add(fileName); } } } } } if (hasArrays) { imports.add('arrays.dart'); } return Array.from(imports).sort(); } private isPrimitive (type: string): boolean { return ['float', 'double', 'int', 'bool', 'size_t', 'int32_t', 'uint32_t'].includes(type); } private isMethodOverride (method: CMethod, cType: CClassOrStruct): boolean { // Static methods cannot be overridden in Dart const isStatic = method.parameters.length === 0 || (method.parameters[0].name !== 'self' && !method.parameters[0].cType.startsWith(cType.name)); if (isStatic) { return false; } // Check if this method exists in parent classes or interfaces const parentName = this.inheritance[cType.name]?.extends; if (parentName) { const parent = this.classMap.get(parentName); if (parent) { const methodSuffix = this.getMethodSuffix(method.name, cType.name); const parentMethodName = `${parentName}_${methodSuffix}`; if (parent.methods.some(m => m.name === parentMethodName)) { return true; } } } // Check interfaces for (const interfaceName of this.inheritance[cType.name]?.mixins || []) { const interfaceClass = this.classMap.get(interfaceName); if (interfaceClass) { const methodSuffix = this.getMethodSuffix(method.name, cType.name); const interfaceMethodName = `${interfaceName}_${methodSuffix}`; if (interfaceClass.methods.some(m => m.name === interfaceMethodName)) { return true; } } } return false; } // Utility methods - keeping from previous implementation private toDartTypeName (cTypeName: string): string { if (cTypeName.startsWith('spine_')) { const name = cTypeName.slice(6); return this.toPascalCase(name); } return this.toPascalCase(cTypeName); } private toCTypeName (dartTypeName: string): string { return `spine_${toSnakeCase(dartTypeName)}`; } private toDartMethodName (cMethodName: string, cTypeName: string): string { const prefix = `${cTypeName}_`; if (cMethodName.startsWith(prefix)) { return this.toCamelCase(cMethodName.slice(prefix.length)); } return this.toCamelCase(cMethodName); } private toDartEnumValueName (cValueName: string, cEnumName: string): string { const enumNameUpper = cEnumName.toUpperCase(); const prefixes = [ `SPINE_${enumNameUpper}_`, `${enumNameUpper}_`, 'SPINE_' ]; let name = cValueName; for (const prefix of prefixes) { if (name.startsWith(prefix)) { name = name.slice(prefix.length); break; } } const enumValue = this.toCamelCase(name.toLowerCase()); if (cEnumName === 'spine_mix_direction' && ['in', 'out'].includes(enumValue)) { return `direction${this.toPascalCase(enumValue)}`; } return enumValue; } private toDartReturnType (cType: string, nullable?: boolean): string { let baseType: string; if (cType === 'void') return 'void'; if (cType === 'char*' || cType === 'char *' || cType === 'const char*' || cType === 'const char *') baseType = 'String'; else if (cType === 'float' || cType === 'double') baseType = 'double'; else if (cType === 'int' || cType === 'size_t' || cType === 'int32_t' || cType === 'uint32_t') baseType = 'int'; else if (cType === 'bool') baseType = 'bool'; // Handle primitive pointer types else if (cType === 'void*' || cType === 'void *') baseType = 'Pointer'; else if (cType === 'float*' || cType === 'float *') baseType = 'Pointer'; else if (cType === 'uint32_t*' || cType === 'uint32_t *') baseType = 'Pointer'; else if (cType === 'uint16_t*' || cType === 'uint16_t *') baseType = 'Pointer'; else if (cType === 'int*' || cType === 'int *') baseType = 'Pointer'; else baseType = this.toDartTypeName(cType); return nullable ? `${baseType}?` : baseType; } private toDartParameterType (param: CParameter): string { if (param.cType === 'char*' || param.cType === 'char *' || param.cType === 'const char*' || param.cType === 'const char *') { return 'String'; } // Handle void* parameters as Pointer if (param.cType === 'void*' || param.cType === 'void *') { return 'Pointer'; } return this.toDartReturnType(param.cType, param.isNullable); } private convertDartToC (dartValue: string, param: CParameter): string { if (param.cType === 'char*' || param.cType === 'char *' || param.cType === 'const char*' || param.cType === 'const char *') { return `${dartValue}.toNativeUtf8().cast()`; } if (this.enumNames.has(param.cType)) { if (param.isNullable) { return `${dartValue}?.value ?? 0`; } return `${dartValue}.value`; } if (param.cType.startsWith('spine_')) { if (param.isNullable) { return `${dartValue}?.nativePtr.cast() ?? Pointer.fromAddress(0)`; } return `${dartValue}.nativePtr.cast()`; } return dartValue; } private generateReturnConversion (cReturnType: string, resultVar: string, nullable?: boolean): string { if (cReturnType === 'char*' || cReturnType === 'char *' || cReturnType === 'const char*' || cReturnType === 'const char *') { if (nullable) { return `return ${resultVar}.address == 0 ? null : ${resultVar}.cast().toDartString();`; } return `return ${resultVar}.cast().toDartString();`; } if (this.enumNames.has(cReturnType)) { const dartType = this.toDartTypeName(cReturnType); if (nullable) { return `return ${resultVar} == 0 ? null : ${dartType}.fromValue(${resultVar});`; } return `return ${dartType}.fromValue(${resultVar});`; } if (cReturnType.startsWith('spine_array_')) { const dartType = this.toDartTypeName(cReturnType); if (nullable) { return `return ${resultVar}.address == 0 ? null : ${dartType}.fromPointer(${resultVar});`; } return `return ${dartType}.fromPointer(${resultVar});`; } if (cReturnType.startsWith('spine_')) { const dartType = this.toDartTypeName(cReturnType); const cClass = this.classMap.get(cReturnType); if (nullable) { if (cClass && this.isAbstract(cClass)) { return `if (${resultVar}.address == 0) return null; ${this.generateRttiBasedInstantiation(dartType, resultVar, cClass)}`; } return `return ${resultVar}.address == 0 ? null : ${dartType}.fromPointer(${resultVar});`; } else { if (cClass && this.isAbstract(cClass)) { return this.generateRttiBasedInstantiation(dartType, resultVar, cClass); } return `return ${dartType}.fromPointer(${resultVar});`; } } return `return ${resultVar};`; } private generateRttiBasedInstantiation (abstractType: string, resultVar: string, abstractClass: CClassOrStruct): string { const lines: string[] = []; const concreteSubclasses = this.getConcreteSubclasses(abstractClass.name); if (concreteSubclasses.length === 0) { return `throw UnsupportedError('Cannot instantiate abstract class ${abstractType} from pointer - no concrete subclasses found');`; } lines.push(`final rtti = SpineBindings.bindings.${abstractClass.name}_get_rtti(${resultVar});`); lines.push(`final className = SpineBindings.bindings.spine_rtti_get_class_name(rtti).cast().toDartString();`); lines.push(`switch (className) {`); for (const subclass of concreteSubclasses) { const dartSubclass = this.toDartTypeName(subclass); lines.push(` case '${subclass}':`); lines.push(` return ${dartSubclass}.fromPointer(${resultVar}.cast());`); } lines.push(` default:`); lines.push(` throw UnsupportedError('Unknown concrete type: \$className for abstract class ${abstractType}');`); lines.push(`}`); return lines.join('\n '); } private getConcreteSubclasses (abstractClassName: string): string[] { const concreteSubclasses: string[] = []; for (const [childName, supertypeList] of Object.entries(this.supertypes || {})) { if (supertypeList.includes(abstractClassName)) { const childClass = this.classMap.get(childName); if (childClass && !this.isAbstract(childClass)) { concreteSubclasses.push(childName); } } } return concreteSubclasses; } private isAbstract (cType: CClassOrStruct): boolean { return cType.cppType.isAbstract === true; } private getConstructorName (constr: CMethod, cType: CClassOrStruct): string { const dartClassName = this.toDartTypeName(cType.name); const cTypeName = this.toCTypeName(dartClassName); let constructorName = constr.name.replace(`${cTypeName}_create`, ''); if (constructorName) { if (/^\d+$/.test(constructorName)) { if (cType.name === 'spine_color' && constr.name === 'spine_color_create2') { constructorName = 'fromRGBA'; } else if (constr.parameters.length > 0) { const firstParamType = constr.parameters[0].cType.replace('*', '').trim(); if (firstParamType === cType.name) { constructorName = 'from'; } else { constructorName = `variant${constructorName}`; } } else { constructorName = `variant${constructorName}`; } } else if (constructorName.startsWith('_')) { constructorName = this.toCamelCase(constructorName.slice(1)); } } return constructorName || dartClassName; } private extractPropertyName (methodName: string, typeName: string): string { const prefix = `${typeName}_`; let name = methodName.startsWith(prefix) ? methodName.slice(prefix.length) : methodName; if (name.startsWith('get_')) { name = name.slice(4); } else if (name.startsWith('set_')) { name = name.slice(4); } if (name === 'update_cache') { return 'updateCacheList'; } const typeNames = ['int', 'float', 'double', 'bool', 'string']; if (typeNames.includes(name.toLowerCase())) { return `${this.toCamelCase(name)}Value`; } return this.toCamelCase(name); } private hasRawPointerParameters (method: CMethod): boolean { for (const param of method.parameters) { if (this.isRawPointer(param.cType)) { return true; } } return false; } private isRawPointer (cType: string): boolean { if (cType === 'char*' || cType === 'char *' || cType === 'const char*' || cType === 'const char *') { return false; } if (cType.includes('*')) { const cleanType = cType.replace('*', '').trim(); if (!cleanType.startsWith('spine_')) { return true; } } return false; } private isMethodInherited (method: CMethod, cType: CClassOrStruct): boolean { const parentName = this.inheritance[cType.name]?.extends; if (!parentName) { return false; } const parent = this.classMap.get(parentName); if (!parent) { return false; } const methodSuffix = this.getMethodSuffix(method.name, cType.name); // Don't consider dispose methods as inherited - they're type-specific if (methodSuffix === 'dispose') { return false; } const parentMethodName = `${parentName}_${methodSuffix}`; const hasInParent = parent.methods.some(m => m.name === parentMethodName); if (hasInParent) { return true; } return this.isMethodInherited(method, parent); } private getMethodSuffix (methodName: string, typeName: string): string { const prefix = `${typeName}_`; if (methodName.startsWith(prefix)) { return methodName.slice(prefix.length); } return methodName; } private renumberMethods (methods: CMethod[], typeName: string): Array<{ method: CMethod, renamedMethod?: string }> { const result: Array<{ method: CMethod, renamedMethod?: string }> = []; const methodGroups = new Map(); for (const method of methods) { const match = method.name.match(/^(.+?)_(\d+)$/); if (match) { const baseName = match[1]; if (!methodGroups.has(baseName)) { methodGroups.set(baseName, []); } methodGroups.get(baseName)!.push(method); } else { result.push({ method }); } } for (const [baseName, groupedMethods] of methodGroups) { if (groupedMethods.length === 1) { const method = groupedMethods[0]; const dartMethodName = this.toDartMethodName(baseName, typeName); result.push({ method, renamedMethod: dartMethodName }); } else { groupedMethods.sort((a, b) => { const aNum = parseInt(a.name.match(/_(\d+)$/)![1]); const bNum = parseInt(b.name.match(/_(\d+)$/)![1]); return aNum - bNum; }); for (let i = 0; i < groupedMethods.length; i++) { const method = groupedMethods[i]; const newNumber = i + 1; const currentNumber = parseInt(method.name.match(/_(\d+)$/)![1]); const baseDartName = this.toDartMethodName(baseName, typeName); if (i === 0) { // First method in the group - remove the number result.push({ method, renamedMethod: baseDartName }); } else if (newNumber !== currentNumber) { // Need to renumber result.push({ method, renamedMethod: `${baseDartName}${newNumber}` }); } else { // Number is correct, no renamed method needed result.push({ method }); } } } } return result; } private needsStringConversions (cType: CClassOrStruct): boolean { for (const method of [...cType.methods, ...cType.constructors]) { if (method.returnType === 'char*' || method.returnType === 'char *' || method.returnType === 'const char*' || method.returnType === 'const char *') { return true; } for (const param of method.parameters) { if (param.cType === 'char*' || param.cType === 'char *' || param.cType === 'const char*' || param.cType === 'const char *') { return true; } } // Check if method returns abstract types (which use RTTI and need Utf8) if (method.returnType.startsWith('spine_')) { const cleanType = method.returnType.replace('*', '').trim(); const returnClass = this.classMap.get(cleanType); if (returnClass && this.isAbstract(returnClass)) { return true; } } } return false; } private async writeExportFile (classes: DartClass[], enums: DartEnum[]): Promise { const lines: string[] = []; lines.push(LICENSE_HEADER); lines.push(''); lines.push('// AUTO GENERATED FILE, DO NOT EDIT.'); lines.push(''); lines.push('// Export all generated types'); lines.push(''); // Export enums if (enums.length > 0) { lines.push('// Enums'); for (const dartEnum of enums) { lines.push(`export '${toSnakeCase(dartEnum.name)}.dart';`); } lines.push(''); } // Export classes if (classes.length > 0) { lines.push('// Classes'); for (const dartClass of classes) { lines.push(`export '${toSnakeCase(dartClass.name)}.dart';`); } lines.push(''); } // Export arrays lines.push('// Arrays'); lines.push(`export 'arrays.dart';`); const filePath = path.join(path.dirname(path.dirname(this.outputDir)), 'lib/generated/api.dart'); fs.writeFileSync(filePath, lines.join('\n')); } private toPascalCase (str: string): string { return str.split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); } private toCamelCase (str: string): string { const pascal = this.toPascalCase(str); return pascal.charAt(0).toLowerCase() + pascal.slice(1); } private async writeWebInitFile(cTypes: CClassOrStruct[], cArrayTypes: CClassOrStruct[]): Promise { const lines: string[] = []; lines.push(LICENSE_HEADER); lines.push(''); lines.push('// AUTO GENERATED FILE, DO NOT EDIT.'); lines.push(''); lines.push('// ignore_for_file: type_argument_not_matching_bounds'); lines.push(`import 'package:flutter/services.dart';`); lines.push(`import 'package:inject_js/inject_js.dart' as js;`); lines.push(`import 'web_ffi/web_ffi.dart';`); lines.push(`import 'web_ffi/web_ffi_modules.dart';`); lines.push(''); lines.push(`import 'generated/spine_dart_bindings_generated.dart';`); lines.push(''); lines.push('Module? _module;'); lines.push(''); lines.push('class SpineDartFFI {'); lines.push(' final DynamicLibrary dylib;'); lines.push(' final Allocator allocator;'); lines.push(''); lines.push(' SpineDartFFI(this.dylib, this.allocator);'); lines.push('}'); lines.push(''); lines.push('Future initSpineDartFFI(bool useStaticLinkage) async {'); lines.push(' if (_module == null) {'); lines.push(' Memory.init();'); lines.push(''); // Collect all wrapper types const wrapperTypes = new Set(); // Add regular types for (const cType of cTypes) { wrapperTypes.add(`${cType.name}_wrapper`); } // Add array types for (const arrayType of cArrayTypes) { wrapperTypes.add(`${arrayType.name}_wrapper`); } // Add special types that might not be in the regular types list wrapperTypes.add('spine_atlas_result_wrapper'); wrapperTypes.add('spine_skeleton_data_result_wrapper'); wrapperTypes.add('spine_skeleton_drawable_wrapper'); wrapperTypes.add('spine_animation_state_events_wrapper'); wrapperTypes.add('spine_skin_entry_wrapper'); wrapperTypes.add('spine_skin_entries_wrapper'); wrapperTypes.add('spine_texture_loader_wrapper'); // Sort and write all registerOpaqueType calls const sortedTypes = Array.from(wrapperTypes).sort(); for (const type of sortedTypes) { lines.push(` registerOpaqueType<${type}>();`); } lines.push(''); lines.push(` await js.importLibrary('assets/packages/spine_flutter/lib/assets/libspine_flutter.js');`); lines.push(` Uint8List wasmBinaries = (await rootBundle.load(`); lines.push(` 'packages/spine_flutter/lib/assets/libspine_flutter.wasm',`); lines.push(` ))`); lines.push(` .buffer`); lines.push(` .asUint8List();`); lines.push(` _module = await EmscriptenModule.compile(wasmBinaries, 'libspine_flutter');`); lines.push(' }'); lines.push(' Module? m = _module;'); lines.push(' if (m != null) {'); lines.push(' final dylib = DynamicLibrary.fromModule(m);'); lines.push(' return SpineDartFFI(dylib, dylib.boundMemory);'); lines.push(' } else {'); lines.push(` throw Exception("Couldn't load libspine-flutter.js/.wasm");`); lines.push(' }'); lines.push('}'); const filePath = path.join(path.dirname(this.outputDir), 'spine_dart_init_web.dart'); fs.writeFileSync(filePath, lines.join('\n')); } }