UNPKG

brick-codegen

Version:

Better React Native native module development

1,245 lines (1,165 loc) 137 kB
import { createRequire } from "node:module"; import path from "path"; import pc from "picocolors"; import fs from "fs-extra"; import { glob } from "glob"; import crypto from "crypto"; import ts from "typescript"; //#region rolldown:runtime var __require = /* @__PURE__ */ createRequire(import.meta.url); //#endregion //#region src/generator.ts var Generator = class { config; platformRegistry; constructor(config, platformRegistry) { this.config = config; this.platformRegistry = platformRegistry; } /** * Main generation method - Generates .brick folder structure with package.json and NativeBrickModule.ts */ async generateTypeScriptFiles(modules) { try { const brickDir = path.join(this.config.projectRoot, this.config.outputTypescript); const srcDir = path.join(brickDir, "src"); const specDir = path.join(srcDir, "spec"); const generatedFiles = []; await fs.ensureDir(specDir); const packageJsonFile = await this.generatePackageJson(brickDir); generatedFiles.push(packageJsonFile); const turboSpecFile = await this.generateTurboModuleSpec(modules, specDir); generatedFiles.push(turboSpecFile); return generatedFiles; } catch (error) { throw new Error(`Failed to generate TypeScript files: ${error instanceof Error ? error.message : String(error)}`); } } /** * Generates package.json for .brick folder */ async generatePackageJson(brickDir) { const projectPackageJsonPath = path.join(this.config.projectRoot, "package.json"); let projectName = Math.random().toString(36).substring(2, 15); try { const projectPackageJson = await fs.readFile(projectPackageJsonPath, "utf-8"); projectName = JSON.parse(projectPackageJson).name || Math.random().toString(36).substring(2, 15); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); } const packageJson = { name: `brick-${crypto.createHash("md5").update(projectName).digest("hex").substring(0, 8)}`, codegenConfig: { name: "BrickModuleSpec", type: "modules", jsSrcsDir: "src/spec" } }; const outputPath = path.join(brickDir, "package.json"); await fs.writeFile(outputPath, JSON.stringify(packageJson, null, 2)); this.logGenerated(outputPath); return outputPath; } /** * Generates TurboModule specification for React Native Codegen * This is the only file generated by brick-codegen */ async generateTurboModuleSpec(modules, specDir) { const methods = this.getAllMethods(modules); const constants = this.getAllConstants(modules); const interfaceDefinitions = []; const seenInterfaces = /* @__PURE__ */ new Set(); for (const module of modules) for (const interfaceDef of module.interfaceDefinitions) if (!interfaceDef.name.includes("ModuleSpec") && !interfaceDef.name.includes("Events")) { const prefixedName = `${module.moduleName}_${interfaceDef.name}`; if (!seenInterfaces.has(prefixedName)) { seenInterfaces.add(prefixedName); const properties = interfaceDef.properties.map((prop) => { const optionalMark = prop.optional ? "?" : ""; let propType = prop.type; for (const otherInterface of module.interfaceDefinitions) if (propType.includes(otherInterface.name) && !otherInterface.name.includes("ModuleSpec") && !otherInterface.name.includes("Events")) propType = propType.replace(new RegExp(`\\b${otherInterface.name}\\b`, "g"), `${module.moduleName}_${otherInterface.name}`); return ` ${prop.name}${optionalMark}: ${propType};`; }).join("\n"); interfaceDefinitions.push(`export interface ${prefixedName} {\n${properties}\n}`); } } const typeMappings = /* @__PURE__ */ new Map(); for (const module of modules) for (const interfaceDef of module.interfaceDefinitions) if (!interfaceDef.name.includes("ModuleSpec") && !interfaceDef.name.includes("Events")) typeMappings.set(interfaceDef.name, `${module.moduleName}_${interfaceDef.name}`); const methodDeclarations = methods.map((method) => { const methodKey = `${method.moduleName}_${method.name}`; let parameterPart = method.signature.substring(method.name.length); typeMappings.forEach((prefixedName, originalName) => { const regex = new RegExp(`\\b${originalName}\\b`, "g"); parameterPart = parameterPart.replace(regex, prefixedName); }); parameterPart = parameterPart.replace(/\bAnyObject\b/g, "{}"); return ` ${methodKey}${parameterPart};`; }).join("\n"); const constantsDeclarations = constants.length > 0 ? `\n // ========== CONSTANTS ==========\n readonly getConstants: () => {\n${constants.map((constant) => { return ` ${`${constant.moduleName}_${constant.name}`}: ${constant.type};`; }).join("\n")}\n };\n` : ""; const specCode = `// This file is automatically generated by brick-codegen for React Native Codegen // Do not edit manually - regenerate using: brick-codegen // Note: This uses the same format as NativeBrickModule.ts for consistency import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; ${interfaceDefinitions.length > 0 ? `\n// ========== TYPE DEFINITIONS ==========\n${interfaceDefinitions.join("\n\n")}\n` : ""} /** * TurboModule specification for React Native Codegen * This file is used by React Native's official codegen to generate native bindings */ export interface Spec extends TurboModule { // ========== MODULE MANAGEMENT ========== /** * Gets list of all registered Brick modules */ getRegisteredModules(): string[]; /** * Adds event listener for a module */ addListener(eventName: string): void; /** * Removes event listener for a module */ removeListeners(count: number): void; ${constantsDeclarations} // ========== GENERATED MODULE METHODS ========== ${methodDeclarations} } export default TurboModuleRegistry.getEnforcing<Spec>('BrickModule'); `; const outputPath = path.join(specDir, "NativeBrickModule.ts"); await fs.writeFile(outputPath, specCode); this.logGenerated(outputPath); return outputPath; } /** * Gets all methods from all modules */ getAllMethods(modules) { const methods = []; for (const module of modules) for (const method of module.methods) methods.push({ ...method, moduleName: module.moduleName }); return methods; } /** * Gets all constants from all modules */ getAllConstants(modules) { const constants = []; for (const module of modules) for (const constant of module.constants) constants.push({ moduleName: module.moduleName, name: constant.name, type: constant.type, readonly: constant.readonly }); return constants; } /** * Generates bridge code for all platforms using DI pattern */ async generateBridgeCode(modules) { const result = { ios: [], android: [], allFiles: [] }; if (!this.platformRegistry) return result; const selectedPlatforms = this.config.selectedPlatforms; if (!selectedPlatforms || selectedPlatforms.includes("ios")) try { const iosFiles = await this.platformRegistry.generateForPlatform("ios", modules); result.ios = iosFiles; result.allFiles.push(...iosFiles); } catch (error) { console.error(`${pc.bold(pc.cyan("[Brick]"))} ${pc.red(`iOS bridge generation failed: ${error instanceof Error ? error.message : String(error)}`)}`); } if (!selectedPlatforms || selectedPlatforms.includes("android")) try { const androidFiles = await this.platformRegistry.generateForPlatform("android", modules); result.android = androidFiles; result.allFiles.push(...androidFiles); } catch (error) { console.error(`${pc.bold(pc.cyan("[Brick]"))} ${pc.red(`Android bridge generation: ${error instanceof Error ? error.message : String(error)}`)}`); } return result; } /** * Main unified generation method - Generates all files including TypeScript specs and bridge code */ async generateAll(modules) { const allGeneratedFiles = []; const tsFiles = await this.generateTypeScriptFiles(modules); allGeneratedFiles.push(...tsFiles); const bridgeResult = await this.generateBridgeCode(modules); allGeneratedFiles.push(...bridgeResult.allFiles); return allGeneratedFiles; } log(message) { console.log(`${pc.bold(pc.green("[Brick]"))} ${message}`); } logGenerated(filepath) { const absolutePath = path.resolve(filepath); console.log(`${pc.bold(pc.green("[Brick]"))} Generated artifact: ${absolutePath}`); } }; //#endregion //#region src/utils/type-helpers.ts /** * Shared type helper utilities for platform generators */ /** * Returns true if the given TS type string represents an untyped object * that should be treated as a generic dictionary/map on native bridges. * * Supported aliases: * - '{}' (empty inline object) * - 'any' * - 'AnyObject' (explicit alias used in specs to mean dictionary-like) */ function isAnyObjectType(tsType) { const t = String(tsType || "").trim(); return t === "{}" || t === "any" || t === "AnyObject"; } //#endregion //#region src/platforms/android-platform.ts var AndroidPlatformGenerator = class { config; currentOutputDir = ""; scannedModules = []; constructor(config) { this.config = config; } getPlatformName() { return "android"; } supports(target) { return target === "android" || target === "java" || target === "kotlin"; } /** * Main method - Generates Android bridge files */ async generateBridge(modules, scannedModules) { if (scannedModules) this.scannedModules = scannedModules; try { const androidBrickDir = path.join(this.config.projectRoot, this.config.outputAndroid); const kotlinOutputDir = path.join(androidBrickDir, "src", "main", "kotlin"); this.currentOutputDir = androidBrickDir; const generatedFiles = []; await fs.ensureDir(kotlinOutputDir); try { const bridgeFile = await this.generateAndroidBridge(modules, kotlinOutputDir); generatedFiles.push(bridgeFile); } catch (error) { throw new Error(`Failed in generateAndroidBridge: ${error.message}`); } try { const implFile = await this.generateKotlinImplementation(modules, kotlinOutputDir); generatedFiles.push(implFile); } catch (error) { throw new Error(`Failed in generateKotlinImplementation: ${error.message}`); } try { const dataClassFiles = await this.generateKotlinDataClasses(modules); generatedFiles.push(...dataClassFiles); } catch (error) { throw new Error(`Failed in generateKotlinDataClasses: ${error.message}`); } try { const localTypeFiles = await this.generateLocalModuleTypes(modules, androidBrickDir); generatedFiles.push(...localTypeFiles); } catch (error) { throw new Error(`Failed in generateLocalModuleTypes: ${error.message}`); } try { await this.generateBrickModuleBase(kotlinOutputDir); generatedFiles.push(path.join(kotlinOutputDir, "BrickModuleBase.kt")); } catch (error) { throw new Error(`Failed in generateBrickModuleBase: ${error.message}`); } try { const gradleFile = await this.generateBuildGradle(); generatedFiles.push(gradleFile); } catch (error) { throw new Error(`Failed in generateBuildGradle: ${error.message}`); } return generatedFiles; } catch (error) { throw new Error(`Failed to generate Android bridge: ${error.message}`); } } isAvailable() { return true; } /** * Generates event support for Kotlin interface */ generateKotlinEventSupport(module) { if (!module.events || module.events.length === 0) return ""; return ` // MARK: - Event Support // Note: Event support is provided by extending BrickModuleSpec // which includes protected sendEvent() method // Supported events: ${module.events.map((e) => e.name).join(", ")}`; } /** * Generates Kotlin data classes for complex types and interfaces */ async generateKotlinDataClasses(modules) { const generatedFiles = []; const allMethods = this.getAllMethods(modules); const processedTypes = /* @__PURE__ */ new Set(); for (const module of modules) { const dataClasses = []; const interfaces = this.extractInterfaceDefinitions(module); for (const interfaceDef of interfaces) if (!processedTypes.has(interfaceDef.name)) { const dataClass = this.generateKotlinDataClassFromInterface(interfaceDef); dataClasses.push(dataClass); processedTypes.add(interfaceDef.name); } if (dataClasses.length > 0) { const content = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package com.brickmodule.codegen import com.google.gson.annotations.SerializedName ${dataClasses.join("\n\n")} `; const outputPath = path.join(this.currentOutputDir, `${module.moduleName}Types.kt`); await fs.writeFile(outputPath, content); this.logGenerated(outputPath); generatedFiles.push(outputPath); } } for (const { method, module } of allMethods) { const dataClasses = []; for (const param of method.params) if (param.type.startsWith("{") && param.type.endsWith("}")) { const className = `Brick${module.moduleName}${this.capitalize(method.name)}${this.capitalize(param.name)}`; if (!processedTypes.has(className)) { const dataClass = this.generateKotlinDataClass(className); dataClasses.push(dataClass); processedTypes.add(className); } } else if (param.type.endsWith("[]")) { const elementType = param.type.slice(0, -2); if (elementType.startsWith("{") && elementType.endsWith("}")) { const className = `Brick${module.moduleName}${this.capitalize(method.name)}${this.capitalize(param.name)}Element`; if (!processedTypes.has(className)) { const dataClass = this.generateKotlinDataClass(className); dataClasses.push(dataClass); processedTypes.add(className); } } } let actualReturnType = method.returnType; if (actualReturnType.startsWith("Promise<") && actualReturnType.endsWith(">")) actualReturnType = actualReturnType.slice(8, -1); if (actualReturnType.startsWith("{") && actualReturnType.endsWith("}")) { const className = `Brick${module.moduleName}${this.capitalize(method.name)}Response`; if (!processedTypes.has(className)) { const dataClass = this.generateKotlinDataClass(className); dataClasses.push(dataClass); processedTypes.add(className); } } if (dataClasses.length > 0) { const content = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package com.brickmodule.codegen import com.google.gson.annotations.SerializedName ${dataClasses.join("\n\n")} `; const outputPath = path.join(this.currentOutputDir, `Brick${module.moduleName}${this.capitalize(method.name)}Types.kt`); await fs.writeFile(outputPath, content); this.logGenerated(outputPath); generatedFiles.push(outputPath); } } return generatedFiles; } /** * Generates a single Kotlin data class from TypeScript inline object type * Note: This is a fallback for inline objects. Most types should come from AST-based interfaces. */ generateKotlinDataClass(className) { console.warn(`[Android] Generating data class for inline object type: ${className}. Consider extracting as interface.`); return `data class ${className}( @SerializedName("data") val data: Any )`; } /** * Enhanced Kotlin type mapping that recognizes interface types */ mapTypeScriptToKotlinWithInterfaces(type) { const basicMapping = this.mapTypeScriptToKotlin(type); if (basicMapping !== "Any") return basicMapping; if (this.isPascalCase(type)) return type; return "Any"; } /** * Gets available brick modules from scanned modules * Uses the scanned modules from Scanner instead of re-scanning */ async detectBrickModules() { const brickModules = []; try { for (const module of this.scannedModules) if (module.hasAndroidImplementation) { const gradleProjectName = module.name.replace(/^@/, "").replace(/\//g, "_"); brickModules.push({ npmName: module.name, gradleProjectName }); } } catch (error) { console.warn(`[ANDROID] Warning: Could not process brick modules: ${error}`); } return brickModules; } /** * Generate build.gradle */ async generateBuildGradle() { const content = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { namespace "com.brickmodule.generated" compileSdkVersion 33 defaultConfig { minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" } sourceSets { main { java { srcDirs = ['src/main/java'] } kotlin { srcDirs = ['src/main/kotlin'] } } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } } dependencies { api 'com.facebook.react:react-android:+' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'com.google.code.gson:gson:2.8.9' // Include brick-module for base interfaces if (project.rootProject.findProject(':brick-module')) { implementation project(':brick-module') } // Auto-detected brick modules ${(await this.detectBrickModules()).map((module) => ` if (project.rootProject.findProject(':${module.gradleProjectName}')) { implementation project(':${module.gradleProjectName}') }`).join("\n")} } `; const outputPath = path.join(this.currentOutputDir, "build.gradle"); await fs.writeFile(outputPath, content); this.logGenerated(outputPath); return outputPath; } mapTypeScriptToKotlin(tsType) { if (tsType.startsWith("Promise<") && tsType.endsWith(">")) { const innerType = tsType.slice(8, -1); return this.mapTypeScriptToKotlin(innerType); } switch (tsType) { case "string": return "String"; case "number": return "Double"; case "boolean": return "Boolean"; case "void": return "Unit"; case "any": return "Any"; default: if (tsType.includes("[]")) { const elementType = tsType.replace("[]", ""); return `List<${this.mapTypeScriptToKotlin(elementType)}>`; } if (tsType.startsWith("{") && tsType.endsWith("}")) { console.warn(`[Android] Object literal type not converted to interface: ${tsType}`); return "Any"; } return tsType; } } /** * Maps TypeScript types to React Native bridge types for Kotlin */ mapTypeScriptToReactNativeKotlin(tsType, isReturnType = false, isOptional = false) { if (tsType.startsWith("Promise<") && tsType.endsWith(">")) return "Unit"; if (tsType === "callback") return "Callback"; let kotlinType; switch (tsType) { case "string": kotlinType = "String"; break; case "number": kotlinType = "Double"; break; case "boolean": kotlinType = "Boolean"; break; case "void": kotlinType = "Unit"; break; case "any": kotlinType = "Any"; break; case "string[]": kotlinType = isReturnType ? "WritableArray" : "ReadableArray"; break; default: if (tsType.includes("[]")) kotlinType = isReturnType ? "WritableArray" : "ReadableArray"; else if (tsType.startsWith("{") && tsType.endsWith("}")) kotlinType = isReturnType ? "WritableMap" : "ReadableMap"; else kotlinType = isReturnType ? "WritableMap" : "ReadableMap"; } if (isOptional && !isReturnType && kotlinType !== "Unit") kotlinType += "?"; return kotlinType; } getAllMethods(modules) { const methods = []; if (!modules || !Array.isArray(modules)) return methods; for (const module of modules) { if (!module || !module.methods || !Array.isArray(module.methods)) continue; for (const method of module.methods) methods.push({ method, module }); } return methods; } getAllEvents(modules) { const events = []; if (!modules || !Array.isArray(modules)) return events; for (const module of modules) { if (!module || !module.events || !Array.isArray(module.events)) continue; for (const event of module.events) events.push({ ...event, moduleName: module.moduleName }); } return events; } getAllConstants(modules) { const constants = []; if (!modules || !Array.isArray(modules)) return constants; for (const module of modules) { if (!module || !module.constants || !Array.isArray(module.constants)) continue; for (const constant of module.constants) constants.push({ ...constant, moduleName: module.moduleName }); } return constants; } /** * Checks if a string matches PascalCase pattern (used for interface names) */ isPascalCase(str) { return /^[A-Z][a-zA-Z0-9]*$/.test(str); } capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } isPrimitiveType(tsType) { let actualType = tsType; if (tsType.startsWith("Promise<") && tsType.endsWith(">")) actualType = tsType.slice(8, -1); return [ "string", "number", "boolean", "void" ].includes(actualType); } logGenerated(filepath) { const absolutePath = path.resolve(filepath); console.log(`${pc.bold(pc.green("[Brick]"))} Generated: ${path.basename(absolutePath)}`); } /** * Generates BrickModuleBase.kt interface */ async generateBrickModuleBase(outputDir) { const content = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package com.brickmodule.codegen /** * Base interface that all Brick modules must implement * Contains only essential properties for module identification */ interface BrickModuleBase { /// The name of the module (required for registration) val moduleName: String } /** * Error types for Brick modules */ open class BrickModuleError(message: String, val errorCode: String) : Exception(message) { class TypeMismatch(message: String) : BrickModuleError("Type mismatch: \$message", "TYPE_ERROR") class ExecutionError(message: String) : BrickModuleError("Execution error: \$message", "EXECUTION_ERROR") class InvalidDefinition(message: String) : BrickModuleError("Invalid definition: \$message", "DEFINITION_ERROR") class MethodNotFound(message: String) : BrickModuleError("Method not found: \$message", "METHOD_NOT_FOUND") class ModuleNotFound(message: String) : BrickModuleError("Module not found: \$message", "MODULE_NOT_FOUND") } `; const outputPath = path.join(outputDir, "BrickModuleBase.kt"); await fs.writeFile(outputPath, content); } /** * Generates the main Android bridge file (BrickModule.kt) - like iOS BrickModule.mm */ async generateAndroidBridge(modules, outputDir) { const allMethods = this.getAllMethods(modules) || []; const allConstants = this.getAllConstants(modules) || []; const allEvents = this.getAllEvents(modules) || []; const methodImplementations = allMethods.map((method) => this.generateKotlinMethod(method.method, method.module)).join("\n\n"); this.generateConstantsGetter(allConstants); const supportedEventsArray = this.generateSupportedEventsArray(allEvents); const bridgeContent = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package com.brickmodule.codegen import com.facebook.fbreact.specs.NativeBrickModuleSpec import com.brickmodule.codegen.BrickModuleImpl import com.brickmodule.BrickModuleRegistry import com.facebook.react.bridge.Callback import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.core.DeviceEventManagerModule /** * Generated BrickModule for React Native * Each module method is implemented as override function */ class BrickModule(private val reactContext: ReactContext) : NativeBrickModuleSpec(reactContext as ReactApplicationContext) { private var hasListeners = false private val brickModuleImpl = BrickModuleImpl(reactContext) companion object { const val NAME = "BrickModule" } init { // Initialize implementation brickModuleImpl.setEventEmitter(this) } override fun getName(): String { return NAME } ${allConstants.length > 0 ? `override fun getTypedExportedConstants(): Map<String, Any?> { val constants: MutableMap<String, Any?> = HashMap() ${this.generateNewArchConstants(allConstants)} return constants }` : ""} override fun getRegisteredModules(): WritableArray { return brickModuleImpl.getRegisteredModules() } // ========== GENERATED METHODS ========== ${methodImplementations} // ========== EVENT EMITTER METHODS ========== fun sendEvent(eventName: String, params: Any?) { if (!hasListeners) { return } reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } override fun addListener(eventName: String) { hasListeners = true } override fun removeListeners(count: Double) { if (count == 0.0) { hasListeners = false } } fun getSupportedEvents(): List<String> { ${supportedEventsArray} } } `; const outputPath = path.join(outputDir, "BrickModule.kt"); await fs.writeFile(outputPath, bridgeContent); this.logGenerated(outputPath); return outputPath; } /** * Generates library imports for data types used in BrickModuleImpl */ generateLibraryImports(modules) { const imports = /* @__PURE__ */ new Set(); for (const module of modules) { var _module$sourceModule$; const androidPackage = !module.sourceModule.brickConfig ? "com.brickmodule.codegen" : ((_module$sourceModule$ = module.sourceModule.brickConfig) === null || _module$sourceModule$ === void 0 || (_module$sourceModule$ = _module$sourceModule$.android) === null || _module$sourceModule$ === void 0 ? void 0 : _module$sourceModule$.package) || "com.brickmodule"; imports.add(`import ${androidPackage}.${module.moduleName}Spec`); for (const interfaceDef of module.interfaceDefinitions || []) imports.add(`import ${androidPackage}.${interfaceDef.name}`); } return Array.from(imports).join("\n"); } /** * Generates Kotlin implementation file (BrickModuleImpl.kt) - like iOS BrickModuleImpl.swift */ async generateKotlinImplementation(modules, outputDir) { const allMethods = this.getAllMethods(modules); const allConstants = this.getAllConstants(modules); const methodImplementations = allMethods.map((method) => this.generateKotlinImplMethod(method.method, method.module)).join("\n\n"); const constantGetters = allConstants.map((constant) => this.generateConstantImplGetter(constant)).join("\n\n"); const implContent = `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package com.brickmodule.codegen ${`import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.Promise import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.bridge.Arguments import com.brickmodule.BrickModuleRegistry import com.brickmodule.BrickModuleRegistrar import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.util.HashMap import java.util.ArrayList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch ${this.generateLibraryImports(modules)}`} class BrickModuleImpl(private val reactContext: ReactContext) { private val gson = Gson() private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) init { // Context is now managed per registration, no need to set globally } fun setEventEmitter(eventEmitter: Any) { // Event emitter is managed by BrickModuleRegistry } // ========== GENERATED METHOD IMPLEMENTATIONS ========== ${methodImplementations} // ========== CONSTANT GETTERS ========== ${constantGetters} // ========== UTILITY METHODS ========== fun getRegisteredModules(): WritableArray { val activity = reactContext.currentActivity ?: throw BrickModuleError.ModuleNotFound("No current activity") if (activity !is BrickModuleRegistrar) { throw BrickModuleError.ModuleNotFound("Current activity does not implement BrickModuleRegistrar") } val moduleNames = activity.getModuleRegistry().getRegisteredModules() val writableArray = Arguments.createArray() moduleNames.forEach { moduleName: String -> writableArray.pushString(moduleName) } return writableArray } // ========== EVENT METHODS ========== fun sendEvent(eventName: String, data: Any?) { val activity = reactContext?.currentActivity ?: throw BrickModuleError.ModuleNotFound("No current activity") if (activity !is BrickModuleRegistrar) { throw BrickModuleError.ModuleNotFound("Current activity does not implement BrickHost") } activity.getModuleRegistry().sendEvent(reactContext, eventName, data) } // ========== CONVERSION HELPERS ========== private fun <T> convertFromReadableMap(readableMap: ReadableMap, type: Class<T>): T { // Convert ReadableMap to a proper HashMap to avoid LinkedHashMap issues val map = convertReadableMapToHashMap(readableMap) val jsonString = gson.toJson(map) return gson.fromJson(jsonString, type) } private fun convertToWritableMap(obj: Any): WritableMap { val jsonString = gson.toJson(obj) val map = gson.fromJson(jsonString, object : TypeToken<Map<String, Any>>() {}.type) as Map<String, Any> return Arguments.makeNativeMap(map) } private fun convertToWritableArray(obj: Any): WritableArray { val jsonString = gson.toJson(obj) val list = gson.fromJson(jsonString, object : TypeToken<List<Any>>() {}.type) as List<Any> val writableArray = Arguments.createArray() list.forEach { item -> when (item) { is String -> writableArray.pushString(item) is Double -> writableArray.pushDouble(item) is Boolean -> writableArray.pushBoolean(item) is Int -> writableArray.pushInt(item) is Map<*, *> -> writableArray.pushMap(Arguments.makeNativeMap(item as Map<String, Any>)) is List<*> -> writableArray.pushArray(Arguments.makeNativeArray(item as ArrayList<Any>)) else -> writableArray.pushString(item.toString()) } } return writableArray } private fun <T> convertFromReadableArray(readableArray: ReadableArray, elementType: Class<T>): List<T> { val list = convertReadableArrayToArrayList(readableArray) val jsonString = gson.toJson(list) val listType = TypeToken.getParameterized(java.util.List::class.java, elementType).type return gson.fromJson(jsonString, listType) } private fun convertStringArrayFromReadableArray(readableArray: ReadableArray): List<String> { val list = ArrayList<String>() for (i in 0..(readableArray.size() - 1)) { val str = readableArray.getString(i) if (str != null) { list.add(str) } } return list } private fun convertDoubleArrayFromReadableArray(readableArray: ReadableArray): List<Double> { val list = ArrayList<Double>() for (i in 0..(readableArray.size() - 1)) { list.add(readableArray.getDouble(i)) } return list } private fun convertReadableMapToHashMap(readableMap: ReadableMap?): HashMap<String, Any?>? { if (readableMap == null) return null val map = HashMap<String, Any?>() val keyIterator = readableMap.keySetIterator() while (keyIterator.hasNextKey()) { val key = keyIterator.nextKey() val value = when (readableMap.getType(key)) { com.facebook.react.bridge.ReadableType.Null -> null com.facebook.react.bridge.ReadableType.Boolean -> readableMap.getBoolean(key) com.facebook.react.bridge.ReadableType.Number -> readableMap.getDouble(key) com.facebook.react.bridge.ReadableType.String -> readableMap.getString(key) com.facebook.react.bridge.ReadableType.Map -> convertReadableMapToHashMap(readableMap.getMap(key)) com.facebook.react.bridge.ReadableType.Array -> convertReadableArrayToArrayList(readableMap.getArray(key)) } map.put(key, value) } return map } private fun convertReadableArrayToArrayList(readableArray: ReadableArray?): ArrayList<Any?>? { if (readableArray == null) return null val list = ArrayList<Any?>() for (i in 0..(readableArray.size() - 1)) { val value = when (readableArray.getType(i)) { com.facebook.react.bridge.ReadableType.Null -> null com.facebook.react.bridge.ReadableType.Boolean -> readableArray.getBoolean(i) com.facebook.react.bridge.ReadableType.Number -> readableArray.getDouble(i) com.facebook.react.bridge.ReadableType.String -> readableArray.getString(i) com.facebook.react.bridge.ReadableType.Map -> convertReadableMapToHashMap(readableArray.getMap(i)) com.facebook.react.bridge.ReadableType.Array -> convertReadableArrayToArrayList(readableArray.getArray(i)) } list.add(value) } return list } } `; const outputPath = path.join(outputDir, "BrickModuleImpl.kt"); await fs.writeFile(outputPath, implContent); this.logGenerated(outputPath); return outputPath; } /** * Generates a Kotlin method for BrickModule (override for New Arch) */ generateKotlinMethod(method, module) { const methodName = `${module.moduleName}_${method.name}`; const params = method.params.map((param) => { const kotlinType = param.isFunction ? "Callback" : this.mapTypeScriptToReactNativeKotlin(param.type, false, param.optional); return `${param.name}: ${kotlinType}`; }).join(", "); const promiseParam = method.isAsync ? params ? ", promise: Promise" : "promise: Promise" : ""; const returnType = method.isAsync ? "Unit" : this.mapTypeScriptToReactNativeKotlin(method.returnType, true); const callParams = method.params.map((param) => param.name).join(", "); const asyncParam = method.isAsync ? callParams ? ", promise" : "promise" : ""; return ` override fun ${methodName}(${params}${promiseParam}): ${returnType} { try { ${method.isAsync ? "" : "return "}brickModuleImpl.${methodName}(${callParams}${asyncParam}) } catch (e: Exception) { ${method.isAsync ? `promise.reject("BRICK_ERROR", e.message, e)` : `throw e`} } }`; } /** * Generates constants getter for BrickModule (Old Architecture) */ generateConstantsGetter(constants) { if (constants.length === 0) return " return HashMap()"; return ` val constants = HashMap<String, Any>() ${constants.map((constant) => { return ` constants.put("${`${constant.moduleName}_${constant.name}`}", brickModuleImpl.${constant.moduleName}_${constant.name}())`; }).join("\n")} return constants`; } /** * Generates constants for New Architecture (getTypedExportedConstants) */ generateNewArchConstants(constants) { if (constants.length === 0) return ""; return constants.map((constant) => { return ` constants["${`${constant.moduleName}_${constant.name}`}"] = brickModuleImpl.${constant.moduleName}_${constant.name}()`; }).join("\n"); } /** * Generates supported events array */ generateSupportedEventsArray(events) { if (events.length === 0) return " return emptyList()"; return ` return listOf(${events.map((event) => `"${event.moduleName}_${event.name}"`).join(", ")})`; } /** * Generates Kotlin implementation method with type conversion */ generateKotlinImplMethod(method, module) { const methodName = `${module.moduleName}_${method.name}`; const params = method.params.map((param) => { const kotlinType = param.isFunction ? "Callback" : this.mapTypeScriptToReactNativeKotlin(param.type, false, param.optional); return `${param.name}: ${kotlinType}`; }).join(", "); const promiseParam = method.isAsync ? params ? ", promise: Promise" : "promise: Promise" : ""; const returnType = method.isAsync ? "Unit" : this.mapTypeScriptToReactNativeKotlin(method.returnType, true); const implementation = this.generateMethodImplementation(method, module); return ` fun ${methodName}(${params}${promiseParam}): ${returnType} { try { ${implementation} } catch (e: Exception) { ${method.isAsync ? `promise.reject("BRICK_ERROR", e.message, e)` : `throw e`} } }`; } /** * Generates constant implementation getter using type-safe casting */ generateConstantImplGetter(constant) { const interfaceName = `${constant.moduleName}Spec`; return ` fun ${constant.moduleName}_${constant.name}(): ${this.mapTypeScriptToKotlin(constant.type)} { try { val activity = reactContext.currentActivity ?: return ${this.getDefaultKotlinReturnValue(constant.type)} if (activity !is BrickModuleRegistrar) { return ${this.getDefaultKotlinReturnValue(constant.type)} } val module = activity.getModuleRegistry().getModuleInstance("${constant.moduleName}") ?: return ${this.getDefaultKotlinReturnValue(constant.type)} val typedModule = module as? ${interfaceName} ?: return ${this.getDefaultKotlinReturnValue(constant.type)} return typedModule.${constant.name} } catch (e: Exception) { return ${this.getDefaultKotlinReturnValue(constant.type)} } }`; } /** * Generates parameter conversions for Kotlin methods with concrete types */ generateParameterConversions(method, module) { const conversions = []; for (const param of method.params) { if (param.isFunction) continue; const bridgeType = this.mapTypeScriptToReactNativeKotlin(param.type, false, param.optional); if (bridgeType === "ReadableMap" || bridgeType === "ReadableMap?") { const t = (param.type || "").trim(); if (isAnyObjectType(t)) continue; const className = this.generateConcreteTypeName(module.moduleName, method.name, param.name, param.type); conversions.push(`val ${param.name}Converted = convertFromReadableMap(${param.name}, ${className}::class.java)`); } else if (bridgeType === "ReadableArray" || bridgeType === "ReadableArray?") { const elementType = param.type.slice(0, -2); if (elementType === "string") conversions.push(`val ${param.name}Converted = convertStringArrayFromReadableArray(${param.name})`); else if (elementType === "number") conversions.push(`val ${param.name}Converted = convertDoubleArrayFromReadableArray(${param.name})`); else { const elementClassName = this.generateConcreteTypeName(module.moduleName, method.name, param.name + "Element", elementType); conversions.push(`val ${param.name}Converted = convertFromReadableArray(${param.name}, ${elementClassName}::class.java)`); } } } return conversions.length > 0 ? conversions.join("\n ") : ""; } /** * Generates concrete type name for TypeScript type */ generateConcreteTypeName(moduleName, methodName, paramName, tsType) { if (!tsType.startsWith("{") && !tsType.endsWith("}") && !tsType.endsWith("[]")) return tsType; const capitalizedMethodName = this.capitalize(methodName); const capitalizedParamName = this.capitalize(paramName); return `Brick${moduleName}${capitalizedMethodName}${capitalizedParamName}`; } /** * Extracts interface definitions from parsed module AST */ extractInterfaceDefinitions(module) { const interfaces = []; for (const interfaceDef of module.interfaceDefinitions) { if (interfaceDef.name.includes("ModuleSpec") || interfaceDef.name.includes("Events")) continue; const properties = interfaceDef.properties.map((prop) => ({ name: prop.name, type: prop.type, optional: prop.optional })); interfaces.push({ name: interfaceDef.name, properties }); } return interfaces; } /** * Generates Kotlin data class from TypeScript interface definition */ generateKotlinDataClassFromInterface(interfaceDef) { const className = interfaceDef.name; const dataClassProperties = interfaceDef.properties.map((prop) => { let kotlinType = this.mapTypeScriptToKotlin(prop.type); if (prop.optional && !kotlinType.endsWith("?")) kotlinType += "?"; const defaultValue = prop.optional ? " = null" : ""; return ` @SerializedName("${prop.name}") val ${prop.name}: ${kotlinType}${defaultValue}`; }); return `data class ${className}( ${dataClassProperties.join(",\n")} )`; } /** * Generates the method implementation including conversions */ generateMethodImplementation(method, module) { const paramConversions = this.generateParameterConversions(method, module); const interfaceName = `${module.moduleName}Spec`; const methodParams = method.params.map((param) => { const bridgeType = this.mapTypeScriptToReactNativeKotlin(param.type, false, param.optional); if (param.isFunction) { var _param$functionType, _param$functionType2, _param$functionType4; const argCount = ((_param$functionType = param.functionType) === null || _param$functionType === void 0 || (_param$functionType = _param$functionType.paramTypes) === null || _param$functionType === void 0 ? void 0 : _param$functionType.length) || 0; if (argCount === 0) return `{ ${param.name}.invoke() }`; const argNames = Array.from({ length: argCount }, (_, i) => `v${i}`); if (argCount === 1 && ((_param$functionType2 = param.functionType) === null || _param$functionType2 === void 0 || (_param$functionType2 = _param$functionType2.paramRestFlags) === null || _param$functionType2 === void 0 ? void 0 : _param$functionType2[0]) === true) { var _param$functionType3; const t0 = (((_param$functionType3 = param.functionType) === null || _param$functionType3 === void 0 || (_param$functionType3 = _param$functionType3.paramTypes) === null || _param$functionType3 === void 0 ? void 0 : _param$functionType3[0]) || "").trim(); const elem0 = t0.endsWith("[]") ? t0.slice(0, -2).trim() : t0; if (elem0 === "any") return `{ v0 -> ${param.name}.invoke(*v0) }`; if (isAnyObjectType(elem0)) return `{ v0 -> ${param.name}.invoke(*v0.toTypedArray()) }`; return `{ v0 -> ${param.name}.invoke(v0) }`; } const convertArg = (t, idx) => { const tt = (t || "").trim(); if (tt === "string" || tt === "number" || tt === "boolean") return `v${idx}`; if (tt.endsWith("[]")) return `convertToWritableArray(v${idx})`; if (isAnyObjectType(tt)) return `v${idx}`; if (tt === "WritableMap" || tt === "ReadableMap" || tt === "WritableArray" || tt === "ReadableArray") return `v${idx}`; if (this.isPascalCase(tt)) return `convertToWritableMap(v${idx})`; return `v${idx}`; }; const convertedArgs = (((_param$functionType4 = param.functionType) === null || _param$functionType4 === void 0 ? void 0 : _param$functionType4.paramTypes) || []).map((t, idx) => convertArg(t, idx)); return `{ ${argNames.join(", ")} -> ${param.name}.invoke(${convertedArgs.join(", ")}) }`; } if (bridgeType === "ReadableMap?" || bridgeType === "ReadableArray?" || bridgeType === "ReadableMap" || bridgeType === "ReadableArray") { const tt = (param.type || "").trim(); if (isAnyObjectType(tt)) return param.name; return `${param.name}Converted`; } return param.name; }).join(", "); const baseImplementation = `// Get the activity and module registry val activity = reactContext.currentActivity ?: throw BrickModuleError.ModuleNotFound("No current activity") if (activity !is BrickModuleRegistrar) { throw BrickModuleError.ModuleNotFound("Current activity does not implement BrickModuleRegistrar") } val module = activity.getModuleRegistry().getModuleInstance("${module.moduleName}") ?: throw BrickModuleError.ModuleNotFound("Module '${module.moduleName}' not found") // Cast to the expected interface val typedModule = module as? ${interfaceName} ?: throw BrickModuleError.TypeMismatch("Module does not implement ${interfaceName}") ${paramConversions ? paramConversions + "\n " : ""}`; if (method.isAsync) { let returnConversion = "promise.resolve(result)"; let actualReturnType = method.returnType; if (actualReturnType.startsWith("Promise<") && actualReturnType.endsWith(">")) actualReturnType = actualReturnType.slice(8, -1); if (actualReturnType === "void") returnConversion = "promise.resolve(null)"; else if (actualReturnType.startsWith("{") && actualReturnType.endsWith("}")) returnConversion = "promise.resolve(convertToWritableMap(result))"; else if (actualReturnType.endsWith("[]")) returnConversion = "promise.resolve(convertToWritableArray(result))"; else if (!this.isPrimitiveType(actualReturnType)) returnConversion = "promise.resolve(convertToWritableMap(result))"; return `${baseImplementation}scope.launch { try { val result = typedModule.${method.name}(${methodParams}) ${returnConversion} } catch (e: Exception) { promise.reject("BRICK_ERROR", e.message, e) } }`; } else { let returnStatement = `return typedModule.${method.name}(${methodParams})`; if (method.returnType.startsWith("{") && method.returnType.endsWith("}")) returnStatement = `val result = typedModule.${method.name}(${methodParams}) return convertToWritableMap(result)`; else if (method.returnType.endsWith("[]")) returnStatement = `val result = typedModule.${method.name}(${methodParams}) return convertToWritableArray(result)`; else if (method.returnType !== "string" && method.returnType !== "number" && method.returnType !== "boolean" && method.returnType !== "void" && !method.returnType.includes("Promise")) returnStatement = `val result = typedModule.${method.name}(${methodParams}) return convertToWritableMap(result)`; return `${baseImplementation}${returnStatement}`; } } /** * Gets default Kotlin return value */ getDefaultKotlinReturnValue(tsType) { switch (tsType) { case "string": return "\"\""; case "number": return "0.0"; case "boolean": return "false"; case "void": return ""; default: if (tsType.includes("[]")) return "WritableNativeArray()"; if (tsType.startsWith("{") && tsType.endsWith("}")) return "WritableNativeMap()"; return "null"; } } /** * Generates local module types in .brick/src/main/java/com/brickmodule/codegen/ directory */ async generateLocalModuleTypes(modules, outputDir) { const generatedFiles = []; const typesDir = path.join(outputDir, "src", "main", "java", "com", "brickmodule", "codegen"); const localModules = modules.filter((m) => { var _m$sourceModule; return !((_m$sourceModule = m.sourceModule) === null || _m$sourceModule === void 0 ? void 0 : _m$sourceModule.brickConfig); }); if (localModules.length === 0) return generatedFiles; await fs.ensureDir(typesDir); for (const module of localModules) { const originalModule = { ...module }; if (!module.sourceModule.brickConfig) module.sourceModule.brickConfig = { android: { package: "com.brickmodule.codegen" } }; const interfaceContent = await this.generateLibraryInterface(module); const interfaceFile = path.join(typesDir, `${module.moduleName}Spec.kt`); await fs.writeFile(interfaceFile, interfaceContent); generatedFiles.push(interfaceFile); this.logGenerated(interfaceFile); if (module.interfaceDefinitions.length > 0) { const typesContent = await this.generateLibraryTypes(module); const typesFile = path.join(typesDir, `${module.moduleName}Types.kt`); await fs.writeFile(typesFile, typesContent); generatedFiles.push(typesFile); this.logGenerated(typesFile); } module.sourceModule.brickConfig = originalModule.sourceModule.brickConfig; } return generatedFiles; } /** * Generates Kotlin types for library mode (public API) */ async generateLibraryTypes(module) { var _module$sourceModule, _module$sourceModule2; const interfaceClasses = module.interfaceDefinitions.map((interfaceDef) => this.generateLibraryKotlinDataClass(interfaceDef)).join("\n\n"); return `// This file is automatically generated by brick-codegen // Do not edit manually - regenerate using: brick-codegen package ${((_mo