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