UNPKG

brick-codegen

Version:

Better React Native native module development

1,382 lines (1,353 loc) 173 kB
import { createRequire } from "node:module"; import path from "path"; import pc from "picocolors"; import crypto from "crypto"; import fs from "fs-extra"; import { glob } from "glob"; 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, ".brick"); const srcDir = path.join(brickDir, "src"); const specDir = path.join(srcDir, "spec"); const generatedFiles = []; if (!this.config.dryRun) 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"); const projectPackage = JSON.parse(projectPackageJson); projectName = projectPackage.name || Math.random().toString(36).substring(2, 15); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); } const nameHash = crypto.createHash("md5").update(projectName).digest("hex").substring(0, 8); const packageJson = { name: `brick-${nameHash}`, codegenConfig: { name: "BrickModuleSpec", type: "modules", jsSrcsDir: "src/spec" } }; const outputPath = path.join(brickDir, "package.json"); if (!this.config.dryRun) { 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); }); return ` ${methodKey}${parameterPart};`; }).join("\n"); const constantsDeclarations = constants.length > 0 ? `\n // ========== CONSTANTS ==========\n readonly getConstants: () => {\n${constants.map((constant) => { const constantKey = `${constant.moduleName}_${constant.name}`; return ` ${constantKey}: ${constant.type};`; }).join("\n")}\n };\n` : ""; const typeDefinitionsSection = interfaceDefinitions.length > 0 ? `\n// ========== TYPE DEFINITIONS ==========\n${interfaceDefinitions.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'; ${typeDefinitionsSection} /** * 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"); if (!this.config.dryRun) { 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/library-generator.ts var LibraryGenerator = class { config; platformRegistry; constructor(config, platformRegistry) { this.config = config; this.platformRegistry = platformRegistry; } /** * Main library generation method - Generates only types/protocols for current library */ async generateLibraryCode(modules) { const allGeneratedFiles = []; if (!this.platformRegistry) { this.log("⚠️ No platform registry available, skipping library generation"); return allGeneratedFiles; } this.log(`📦 Generating library code for ${modules.length} modules...`); if (this.shouldGenerateIos()) try { const iosFiles = await this.generateIosLibraryCode(modules); allGeneratedFiles.push(...iosFiles); this.log(`✅ Generated ${iosFiles.length} iOS library files`); } catch (error) { this.logError(`iOS library generation failed: ${error instanceof Error ? error.message : String(error)}`); } if (this.shouldGenerateAndroid()) try { const androidFiles = await this.generateAndroidLibraryCode(modules); allGeneratedFiles.push(...androidFiles); this.log(`✅ Generated ${androidFiles.length} Android library files`); } catch (error) { this.logError(`Android library generation failed: ${error instanceof Error ? error.message : String(error)}`); } return allGeneratedFiles; } /** * Generates iOS library code (Swift types and protocols) */ async generateIosLibraryCode(modules) { const generatedFiles = []; const iosDir = this.config.outputIos || path.join(this.config.projectRoot, "ios"); if (!this.config.dryRun) await fs.ensureDir(iosDir); for (const module of modules) try { const typesFile = await this.generateSwiftTypes(module, iosDir); generatedFiles.push(typesFile); const protocolFile = await this.generateSwiftProtocol(module, iosDir); generatedFiles.push(protocolFile); this.log(`📱 Generated iOS library files for ${module.moduleName}`); } catch (error) { this.logError(`Failed to generate iOS library files for ${module.moduleName}: ${error instanceof Error ? error.message : String(error)}`); } return generatedFiles; } /** * Generates Android library code (Kotlin types and interfaces) */ async generateAndroidLibraryCode(modules) { const generatedFiles = []; const baseAndroidDir = this.config.outputAndroid || path.join(this.config.projectRoot, "android"); for (const module of modules) try { var _module$sourceModule; const androidPackage = ((_module$sourceModule = module.sourceModule) === null || _module$sourceModule === void 0 ? void 0 : _module$sourceModule.androidPackage) || "com.brickmodule"; const packagePath = androidPackage.replace(/\./g, "/"); const androidDir = path.join(baseAndroidDir, "src", "main", "kotlin", packagePath); if (!this.config.dryRun) await fs.ensureDir(androidDir); const typesFile = await this.generateKotlinTypes(module, androidDir); generatedFiles.push(typesFile); const interfaceFile = await this.generateKotlinInterface(module, androidDir); generatedFiles.push(interfaceFile); this.log(`🤖 Generated Android library files for ${module.moduleName} in ${packagePath}`); } catch (error) { this.logError(`Failed to generate Android library files for ${module.moduleName}: ${error instanceof Error ? error.message : String(error)}`); } return generatedFiles; } /** * Generates Swift types file for library */ async generateSwiftTypes(module, outputDir) { const filename = `${module.moduleName}Types.swift`; const outputPath = path.join(outputDir, filename); if (this.platformRegistry) { const iosPlatform = this.platformRegistry.getPlatform("ios"); if (iosPlatform && "generateLibraryTypes" in iosPlatform) { const content = await iosPlatform.generateLibraryTypes(module); if (!this.config.dryRun) { await fs.writeFile(outputPath, content); this.logGenerated(outputPath); } } } return outputPath; } /** * Generates Swift protocol file for library */ async generateSwiftProtocol(module, outputDir) { const filename = `${module.moduleName}TypeModule.swift`; const outputPath = path.join(outputDir, filename); if (this.platformRegistry) { const iosPlatform = this.platformRegistry.getPlatform("ios"); if (iosPlatform && "generateLibraryProtocol" in iosPlatform) { const content = await iosPlatform.generateLibraryProtocol(module); if (!this.config.dryRun) { await fs.writeFile(outputPath, content); this.logGenerated(outputPath); } } } return outputPath; } /** * Generates Kotlin types file for library */ async generateKotlinTypes(module, outputDir) { const filename = `${module.moduleName}Types.kt`; const outputPath = path.join(outputDir, filename); if (this.platformRegistry) { const androidPlatform = this.platformRegistry.getPlatform("android"); if (androidPlatform && "generateLibraryTypes" in androidPlatform) { const content = await androidPlatform.generateLibraryTypes(module); if (!this.config.dryRun) { await fs.writeFile(outputPath, content); this.logGenerated(outputPath); } } } return outputPath; } /** * Generates Kotlin interface file for library */ async generateKotlinInterface(module, outputDir) { const filename = `${module.moduleName}TypeModule.kt`; const outputPath = path.join(outputDir, filename); if (this.platformRegistry) { const androidPlatform = this.platformRegistry.getPlatform("android"); if (androidPlatform && "generateLibraryInterface" in androidPlatform) { const content = await androidPlatform.generateLibraryInterface(module); if (!this.config.dryRun) { await fs.writeFile(outputPath, content); this.logGenerated(outputPath); } } } return outputPath; } /** * Checks if iOS generation should be performed */ shouldGenerateIos() { return true; } /** * Checks if Android generation should be performed */ shouldGenerateAndroid() { return this.config.enableAndroid !== false; } log(message) { console.log(`${pc.bold(pc.green("[LibraryGen]"))} ${message}`); } logError(message) { console.error(`${pc.bold(pc.green("[LibraryGen]"))} ${pc.red(message)}`); } logGenerated(filepath) { const absolutePath = path.resolve(filepath); console.log(`${pc.bold(pc.green("[LibraryGen]"))} Generated artifact: ${absolutePath}`); } }; //#endregion //#region src/parser.ts var Parser = class { config; checker; interfaceDefinitions = /* @__PURE__ */ new Map(); generatedInterfaces = /* @__PURE__ */ new Map(); constructor(config) { this.config = config; } /** * Parses a module specification file */ async parseModuleSpec(module) { try { this.log(`📝 Parsing spec file: ${module.specPath}`); this.interfaceDefinitions.clear(); this.generatedInterfaces.clear(); const specContent = await fs.readFile(module.specPath, "utf8"); const sourceFile = this.createSourceFile(module.specPath, specContent); const parsedModules = this.extractModuleInfo(sourceFile, module); for (const parsed of parsedModules) this.validateParsedModule(parsed); this.log(`✅ Successfully parsed ${parsedModules.length} modules with total ${parsedModules.reduce((sum, m) => sum + m.methods.length, 0)} methods`); return parsedModules; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to parse spec for ${module.name}: ${errorMessage}`); } } /** * Creates TypeScript source file */ createSourceFile(fileName, sourceText) { try { return ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`TypeScript parsing failed: ${errorMessage}`); } } /** * Extracts module information from source file */ extractModuleInfo(sourceFile, sourceModule) { const modules = []; const modulesByName = /* @__PURE__ */ new Map(); const extractedInterfaces = []; ts.forEachChild(sourceFile, (node) => { if (ts.isInterfaceDeclaration(node)) { this.interfaceDefinitions.set(node.name.text, node); const interfaceDef = this.extractInterfaceDefinition(node); if (interfaceDef && !interfaceDef.name.includes("ModuleSpec") && !interfaceDef.name.includes("Events")) extractedInterfaces.push(interfaceDef); } }); ts.forEachChild(sourceFile, (node) => { if (ts.isInterfaceDeclaration(node) && this.isModuleSpecInterface(node)) { const tempResult = { name: sourceModule.name, moduleName: "", version: sourceModule.version, specPath: sourceModule.specPath, constants: [], methods: [], events: [], sourceModule, interfaceDefinitions: extractedInterfaces }; this.processInterface(node, tempResult); if (tempResult.moduleName) modulesByName.set(tempResult.moduleName, tempResult); } }); modules.push(...modulesByName.values()); for (const module of modules) module.interfaceDefinitions.push(...this.generatedInterfaces.values()); if (modules.length === 0) { const defaultModule = { name: sourceModule.name, moduleName: this.capitalizeFirst(sourceModule.name.replace(/-/g, "")), version: sourceModule.version, specPath: sourceModule.specPath, constants: [], methods: [], events: [], sourceModule, interfaceDefinitions: extractedInterfaces }; this.visitNode(sourceFile, defaultModule); defaultModule.interfaceDefinitions.push(...this.generatedInterfaces.values()); modules.push(defaultModule); } return modules; } /** * Visits a TypeScript AST node */ visitNode(node, result) { if (ts.isInterfaceDeclaration(node)) { this.interfaceDefinitions.set(node.name.text, node); this.processInterface(node, result); } else if (ts.isTypeAliasDeclaration(node)) this.processTypeAlias(node, result); else if (ts.isVariableStatement(node)) this.processVariableStatement(node, result); ts.forEachChild(node, (child) => this.visitNode(child, result)); } /** * Processes interface declaration */ processInterface(node, result) { if (!this.isModuleSpecInterface(node)) return; const interfaceName = node.name.text; this.log(`Processing interface: ${interfaceName}`); if (!result.moduleName) result.moduleName = this.extractModuleNameFromInterface(interfaceName); for (const member of node.members) if (ts.isPropertySignature(member)) this.extractProperty(member, result); else if (ts.isMethodSignature(member)) this.extractMethod(member, result); } /** * Processes type alias declaration */ processTypeAlias(node, result) { if (!this.isModuleSpecType(node)) return; if (ts.isTypeLiteralNode(node.type)) { for (const member of node.type.members) if (ts.isPropertySignature(member)) this.extractProperty(member, result); else if (ts.isMethodSignature(member)) this.extractMethod(member, result); } } /** * Processes variable statement */ processVariableStatement(node, result) { if (this.hasExportModifier(node)) { for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) { const constant = { name: declaration.name.text, type: this.inferTypeFromValue(declaration.initializer), value: this.extractLiteralValue(declaration.initializer), readonly: (node.declarationList.flags & ts.NodeFlags.Const) !== 0, jsDocComment: this.extractJSDocComment(node) }; result.constants.push(constant); } } } /** * Checks if an interface is a module spec interface */ isModuleSpecInterface(node) { const name = node.name.text; return name.includes("Spec") || name.includes("Module") || name.includes("Interface"); } /** * Extracts module name from interface name * Example: "CalculatorModuleSpec" -> "Calculator" */ extractModuleNameFromInterface(interfaceName) { const suffixes = [ "ModuleSpec", "Module", "Spec", "Interface", "Type" ]; for (const suffix of suffixes) if (interfaceName.endsWith(suffix)) return interfaceName.slice(0, -suffix.length); return interfaceName; } /** * Checks if a type alias is a module spec type */ isModuleSpecType(node) { const name = node.name.text; return name.includes("Spec") || name.includes("Module") || name.includes("Type"); } /** * Checks if node has export modifier */ hasExportModifier(node) { if (ts.canHaveModifiers(node)) { const modifiers = ts.getModifiers(node); return (modifiers === null || modifiers === void 0 ? void 0 : modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) ?? false; } return false; } /** * Extracts property from interface/type */ extractProperty(node, result) { var _ts$getModifiers; if (!node.name || !ts.isIdentifier(node.name)) return; const propertyName = node.name.text; const isReadonly = ts.canHaveModifiers(node) ? ((_ts$getModifiers = ts.getModifiers(node)) === null || _ts$getModifiers === void 0 ? void 0 : _ts$getModifiers.some((mod) => mod.kind === ts.SyntaxKind.ReadonlyKeyword)) ?? false : false; const typeAnnotation = this.getTypeString(node.type); switch (propertyName) { case "moduleName": if (node.type && ts.isLiteralTypeNode(node.type) && ts.isStringLiteral(node.type.literal)) result.moduleName = node.type.literal.text; break; case "version": break; case "constants": if (node.type && ts.isTypeLiteralNode(node.type)) this.extractConstantsFromType(node.type, result); break; case "supportedEvents": this.extractSupportedEvents(node, result); break; default: if (propertyName !== "moduleName" && propertyName !== "version" && propertyName !== "supportedEvents") result.constants.push({ name: propertyName, type: typeAnnotation, readonly: isReadonly, jsDocComment: this.extractJSDocComment(node) }); } } /** * Extracts method from interface */ extractMethod(node, result) { if (!node.name || !ts.isIdentifier(node.name)) return; const methodName = node.name.text; const params = this.extractParameters(node.parameters); const returnType = this.getTypeString(node.type); const isAsync = this.isPromiseType(returnType); const method = { name: methodName, params, returnType, isAsync, isSync: !isAsync, signature: this.buildMethodSignature(methodName, params, returnType), jsDocComment: this.extractJSDocComment(node), deprecated: this.isDeprecated(node) }; result.methods.push(method); } /** * Extracts constants from a type literal */ extractConstantsFromType(typeLiteral, result) { for (const member of typeLiteral.members) if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) { var _ts$getModifiers2; const constant = { name: member.name.text, type: this.getTypeString(member.type), readonly: ts.canHaveModifiers(member) ? ((_ts$getModifiers2 = ts.getModifiers(member)) === null || _ts$getModifiers2 === void 0 ? void 0 : _ts$getModifiers2.some((mod) => mod.kind === ts.SyntaxKind.ReadonlyKeyword)) ?? true : true, jsDocComment: this.extractJSDocComment(member) }; result.constants.push(constant); } } /** * Extracts supported events from array literal */ extractSupportedEvents(node, result) { if (!result.events) result.events = []; if (node.type && ts.isTupleTypeNode(node.type)) { for (const element of node.type.elements) if (ts.isLiteralTypeNode(element) && ts.isStringLiteral(element.literal)) { const eventName = element.literal.text; result.events.push({ name: eventName, jsDocComment: this.extractJSDocComment(node) }); } } else if (node.type && ts.isArrayTypeNode(node.type)) { const elementType = node.type.elementType; if (ts.isLiteralTypeNode(elementType) && ts.isStringLiteral(elementType.literal)) result.events.push({ name: elementType.literal.text, jsDocComment: this.extractJSDocComment(node) }); } this.log(`Extracted ${result.events.length} supported events`); } /** * Extracts parameters from method signature */ extractParameters(parameters) { const params = []; for (const param of parameters) if (ts.isIdentifier(param.name)) params.push({ name: param.name.text, type: this.getTypeString(param.type), optional: !!param.questionToken, defaultValue: param.initializer ? this.extractDefaultValue(param.initializer) : void 0, jsDocComment: this.extractJSDocComment(param) }); return params; } /** * Extracts default value from expression */ extractDefaultValue(node) { if (ts.isNumericLiteral(node)) return Number(node.text); if (ts.isStringLiteral(node)) return node.text; if (node.kind === ts.SyntaxKind.TrueKeyword) return true; if (node.kind === ts.SyntaxKind.FalseKeyword) return false; if (node.kind === ts.SyntaxKind.NullKeyword) return null; if (node.kind === ts.SyntaxKind.UndefinedKeyword) return void 0; return void 0; } /** * Gets type string from type node */ getTypeString(typeNode) { if (!typeNode) return "any"; return this.typeToString(typeNode); } /** * Generates an interface name for nested object types */ generateNestedInterfaceName(parentName, propertyName) { const capitalizedProperty = propertyName.charAt(0).toUpperCase() + propertyName.slice(1); return `${parentName}${capitalizedProperty}`; } /** * Extracts nested object type as interface definition */ extractNestedInterface(typeLiteral, parentName, propertyName) { const interfaceName = this.generateNestedInterfaceName(parentName, propertyName); if (this.generatedInterfaces.has(interfaceName)) return interfaceName; const properties = []; for (const member of typeLiteral.members) if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) { const propName = member.name.text; let propType; if (member.type && ts.isTypeLiteralNode(member.type)) propType = this.extractNestedInterface(member.type, interfaceName, propName); else propType = this.getTypeString(member.type); const optional = !!member.questionToken; properties.push({ name: propName, type: propType, optional, jsDocComment: this.extractJSDocComment(member) }); } const interfaceDef = { name: interfaceName, properties, jsDocComment: `Auto-generated interface for ${parentName}.${propertyName}` }; this.generatedInterfaces.set(interfaceName, interfaceDef); return interfaceName; } /** * Converts TypeScript type to string representation */ typeToString(type, parentContext) { var _type$getText; switch (type.kind) { case ts.SyntaxKind.StringKeyword: return "string"; case ts.SyntaxKind.NumberKeyword: return "number"; case ts.SyntaxKind.BooleanKeyword: return "boolean"; case ts.SyntaxKind.VoidKeyword: return "void"; case ts.SyntaxKind.AnyKeyword: return "any"; case ts.SyntaxKind.UnknownKeyword: return "unknown"; case ts.SyntaxKind.NullKeyword: return "null"; case ts.SyntaxKind.UndefinedKeyword: return "undefined"; } if (ts.isArrayTypeNode(type)) { const elementType = this.typeToString(type.elementType); return `${elementType}[]`; } if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) { var _type$typeArguments; const typeName = type.typeName.text; if (typeName === "Promise" && ((_type$typeArguments = type.typeArguments) === null || _type$typeArguments === void 0 ? void 0 : _type$typeArguments[0])) return `Promise<${this.typeToString(type.typeArguments[0])}>`; return typeName; } if (ts.isLiteralTypeNode(type)) { if (ts.isStringLiteral(type.literal)) return `"${type.literal.text}"`; if (ts.isNumericLiteral(type.literal)) return type.literal.text; if (type.literal.kind === ts.SyntaxKind.TrueKeyword) return "true"; if (type.literal.kind === ts.SyntaxKind.FalseKeyword) return "false"; } if (ts.isUnionTypeNode(type)) return type.types.map((t) => this.typeToString(t)).join(" | "); if (ts.isTypeLiteralNode(type)) { if (parentContext) return this.extractNestedInterface(type, parentContext.parentName, parentContext.propertyName); const members = type.members.map((member) => { if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) { var _ts$getModifiers3; const key = member.name.text; const valueType = member.type ? this.typeToString(member.type) : "any"; const optional = member.questionToken ? "?" : ""; const readonly = ts.canHaveModifiers(member) && ((_ts$getModifiers3 = ts.getModifiers(member)) === null || _ts$getModifiers3 === void 0 ? void 0 : _ts$getModifiers3.some((mod) => mod.kind === ts.SyntaxKind.ReadonlyKeyword)) ? "readonly " : ""; return `${readonly}${key}${optional}: ${valueType}`; } return ""; }).filter(Boolean); return `{${members.join("; ")}}`; } return ((_type$getText = type.getText) === null || _type$getText === void 0 ? void 0 : _type$getText.call(type)) || "any"; } /** * Checks if a type is a Promise type */ isPromiseType(typeString) { return typeString.startsWith("Promise<"); } /** * Builds method signature string */ buildMethodSignature(name, params, returnType) { const paramStrings = params.map((param) => { const optional = param.optional ? "?" : ""; return `${param.name}${optional}: ${param.type}`; }); return `${name}(${paramStrings.join(", ")}): ${returnType}`; } /** * Extracts JSDoc comment from node */ extractJSDocComment(node) { const sourceFile = node.getSourceFile(); const fullText = sourceFile.getFullText(); const nodeStart = node.getFullStart(); const nodeEnd = node.getStart(); const leadingTrivia = fullText.substring(nodeStart, nodeEnd); const jsDocMatch = leadingTrivia.match(/\/\*\*([\s\S]*?)\*\//); if (jsDocMatch && jsDocMatch[1]) return jsDocMatch[1].trim(); return void 0; } /** * Checks if node is marked as deprecated */ isDeprecated(node) { const jsDoc = this.extractJSDocComment(node); return (jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.includes("@deprecated")) || false; } /** * Infers type from value expression */ inferTypeFromValue(node) { if (ts.isStringLiteral(node)) return "string"; if (ts.isNumericLiteral(node)) return "number"; if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return "boolean"; if (ts.isArrayLiteralExpression(node)) return "any[]"; if (ts.isObjectLiteralExpression(node)) return "object"; return "any"; } /** * Extracts literal value from expression */ extractLiteralValue(node) { if (ts.isStringLiteral(node)) return node.text; if (ts.isNumericLiteral(node)) return Number(node.text); if (node.kind === ts.SyntaxKind.TrueKeyword) return true; if (node.kind === ts.SyntaxKind.FalseKeyword) return false; if (node.kind === ts.SyntaxKind.NullKeyword) return null; return void 0; } /** * Validates parsed module structure */ validateParsedModule(parsed) { if (!parsed.moduleName) throw new Error("Module name not found in spec"); if (parsed.methods.length === 0) throw new Error("No methods found in module spec"); const methodNames = /* @__PURE__ */ new Set(); for (const method of parsed.methods) { if (methodNames.has(method.name)) throw new Error(`Duplicate method name: ${method.name}`); methodNames.add(method.name); if (!this.isValidMethodName(method.name)) throw new Error(`Invalid method name: ${method.name}`); } const constantNames = /* @__PURE__ */ new Set(); for (const constant of parsed.constants) { if (constantNames.has(constant.name)) throw new Error(`Duplicate constant name: ${constant.name}`); constantNames.add(constant.name); } if (parsed.events && parsed.events.length > 0) { const eventNames = /* @__PURE__ */ new Set(); for (const event of parsed.events) { if (eventNames.has(event.name)) throw new Error(`Duplicate event name: ${event.name}`); eventNames.add(event.name); if (!this.isValidEventName(event.name)) throw new Error(`Invalid event name: ${event.name}`); } } } /** * Validates method name conventions */ isValidMethodName(name) { return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(name); } /** * Validates event name conventions */ isValidEventName(name) { return /^[a-zA-Z][a-zA-Z0-9]*$/.test(name); } /** * Validates naming conventions */ validateNamingConventions(parsed) { const warnings = []; if (!this.isCapitalCase(parsed.moduleName)) warnings.push(`Module name '${parsed.moduleName}' should be in PascalCase`); for (const method of parsed.methods) { if (!this.isCamelCase(method.name)) warnings.push(`Method '${method.name}' should be in camelCase`); if (method.isAsync && !method.name.startsWith("get") && !method.name.includes("async")) warnings.push(`Async method '${method.name}' should indicate async nature in name`); } for (const constant of parsed.constants) if (!this.isConstantCase(constant.name)) warnings.push(`Constant '${constant.name}' should be in CONSTANT_CASE`); return warnings; } /** * Validates types in the parsed module */ async validateTypes(parsed) { const errors = []; for (const method of parsed.methods) { if (!this.isValidType(method.returnType)) errors.push(`Invalid return type '${method.returnType}' in method '${method.name}'`); for (const param of method.params) if (!this.isValidType(param.type)) errors.push(`Invalid parameter type '${param.type}' for '${param.name}' in method '${method.name}'`); } return errors; } /** * Checks if type is valid */ isValidType(type) { const validTypes = [ "string", "number", "boolean", "void", "any", "unknown", "null", "undefined", "object", "Array", "Promise" ]; return validTypes.some((validType) => type.includes(validType) || type.match(/^[A-Z][a-zA-Z0-9]*$/)); } isCapitalCase(str) { return /^[A-Z][a-zA-Z0-9]*$/.test(str); } isCamelCase(str) { return /^[a-z][a-zA-Z0-9]*$/.test(str); } isConstantCase(str) { return /^[A-Z][A-Z0-9_]*$/.test(str); } /** * Extracts interface definition from TypeScript AST */ extractInterfaceDefinition(node) { const name = node.name.text; const properties = []; for (const member of node.members) if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) { const propName = member.name.text; let propType; if (member.type && ts.isTypeLiteralNode(member.type)) propType = this.extractNestedInterface(member.type, name, propName); else propType = this.getTypeString(member.type); const optional = !!member.questionToken; properties.push({ name: propName, type: propType, optional, jsDocComment: this.extractJSDocComment(member) }); } return { name, properties, jsDocComment: this.extractJSDocComment(node) }; } capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } log(...args) { if (this.config.verbose) console.log(pc.gray("[parser]"), ...args); } }; //#endregion //#region src/platforms/android-platform.ts var AndroidPlatformGenerator = class { config; isNewArch = false; currentOutputDir = ""; scannedModules = []; constructor(config) { this.config = config; } /** * Detects if New Architecture is enabled by checking gradle.properties */ async isNewArchitecture() { try { const androidDir = path.join(this.config.projectRoot, "android"); const gradlePropertiesPath = path.join(androidDir, "gradle.properties"); if (await fs.pathExists(gradlePropertiesPath)) { const content = await fs.readFile(gradlePropertiesPath, "utf-8"); const lines = content.split("\n"); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith("newArchEnabled") && !trimmedLine.startsWith("#")) { var _trimmedLine$split$; const value = (_trimmedLine$split$ = trimmedLine.split("=")[1]) === null || _trimmedLine$split$ === void 0 ? void 0 : _trimmedLine$split$.trim().toLowerCase(); return value === "true"; } } } return false; } catch (error) { console.warn(`Warning: Could not read gradle.properties: ${error.message}`); return false; } } 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 { this.isNewArch = await this.isNewArchitecture(); const androidDir = path.join(this.config.projectRoot, "android"); const androidBrickDir = path.join(androidDir, ".brick"); 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 { 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 of allMethods) { const dataClasses = []; for (const param of method.params) if (param.type.startsWith("{") && param.type.endsWith("}")) { const className = `Brick${method.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${method.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${method.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${method.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.`); const simpleProperty = ` @SerializedName("data") val data: Any`; return `data class ${className}( ${simpleProperty} )`; } /** * 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 brickModules = await this.detectBrickModules(); const brickModuleDependencies = brickModules.map((module) => ` if (project.rootProject.findProject(':${module.gradleProjectName}')) { implementation project(':${module.gradleProjectName}') }`).join("\n"); 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-native:+' 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 ${brickModuleDependencies} } `; 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"; 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, moduleName: module.moduleName }); } 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