brick-codegen
Version:
Better React Native native module development
1,245 lines (1,165 loc) • 137 kB
JavaScript
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