UNPKG

brick-module

Version:

Better React Native native module development

218 lines (186 loc) 6.84 kB
/** * Brick Module Framework - Main API * Type-safe access to native modules with automatic code generation */ import type { TurboModule } from "react-native"; import { TurboModuleRegistry } from "react-native"; import { BrickError } from "./BrickError"; /** * Base interface that all Brick module specs must extend */ export interface BrickModuleInterface extends TurboModule { readonly moduleName: string; } /** * Type alias for module specifications * Use this as the base interface when defining your module specs * @example * export interface MyModuleSpec extends BrickModuleSpec { * readonly moduleName: "MyModule"; * readonly supportedEvents: ["eventA", "eventB"]; * myMethod(param: string): Promise<string>; * } */ export type BrickModuleSpec = BrickModuleInterface; // Module-level state (previously static class members) const moduleCache = new Map<string, any>(); let nativeModule: any = null; /** * Gets the native TurboModule instance * @private */ function getNativeModule() { if (!nativeModule) { nativeModule = TurboModuleRegistry.getEnforcing("BrickModule"); } return nativeModule; } /** * Enhanced typed module interface with event listeners */ export type BrickModuleWithEvents<T extends BrickModuleInterface> = T; /** * Gets a typed module instance by name with explicit type parameter * @param moduleName - The exact name of the module as defined in its spec * @returns Typed module interface with all methods, constants, and event listeners */ function get<T extends BrickModuleInterface>( moduleName: string ): BrickModuleWithEvents<T> { // Check cache first const cacheKey = moduleName; if (moduleCache.has(cacheKey)) { return moduleCache.get(cacheKey); } const nativeModuleInstance = getNativeModule(); // Cache constants for this module (computed once, reused) let constantsCache: Record<string, any> | null = null; // Create a proxy that intercepts method calls and forwards them to the native module const moduleProxy = new Proxy({} as BrickModuleWithEvents<T>, { get: (_target, property: string | symbol) => { if (typeof property !== "string") { return undefined; } // New-style event subscription: onXxx((event) => void) → EventSubscription if ( property.startsWith("on") && property.length > 2 && property[2] === property[2].toUpperCase() ) { return (listener: (event: unknown) => void) => { // Respect RN CodegenTypes.EventEmitter: call `${moduleName}_onXxx` and return as-is const nativeEventMethod = `${moduleName}_${property}`; const nativeFn = (nativeModuleInstance as any)[nativeEventMethod]; if (typeof nativeFn === "function") { return nativeFn(listener); } throw new Error( `${nativeEventMethod} is not available. Did you run RN codegen?` ); }; } // Special handling for getConstants method (with caching) if (property === "getConstants") { return () => { if (constantsCache !== null) { return constantsCache; } const allConstants = nativeModuleInstance?.getConstants?.() ?? {}; const moduleConstants: Record<string, any> = {}; const constantPrefix = `${moduleName}_`; for (const key in allConstants) { if (key.startsWith(constantPrefix)) { const propName = key.substring(constantPrefix.length); moduleConstants[propName] = allConstants[key]; } } constantsCache = moduleConstants; return moduleConstants; }; } // Handle method calls return (...args: any[]) => { const methodKey = `${moduleName}_${property}`; // Try direct method call (generated by codegen) if (typeof nativeModuleInstance[methodKey] === "function") { const result = nativeModuleInstance[methodKey](...args); // Check if result is a wrapped sync method result // Sync methods have explicit "~sync": true marker for reliable detection // Format: { "~sync": true, success: true, value: T } | { "~sync": true, success: false, error: {...} } if ( result && typeof result === "object" && result["~sync"] === true ) { if (result.success === true) { // Success: return the unwrapped value return result.value; } else { // Failure: throw a BrickError with all properties from error object const errorInfo = result.error || {}; const message = errorInfo.message || errorInfo.errorMessage || "Unknown error"; const code = errorInfo.code || errorInfo.errorCode || "BRICK_ERROR"; // Build userInfo from additional properties const userInfo: Record<string, unknown> = { code }; for (const key of Object.keys(errorInfo)) { if (key !== "code" && key !== "message" && key !== "errorCode" && key !== "errorMessage") { userInfo[key] = errorInfo[key]; } } throw new BrickError(message, code, userInfo, moduleName); } } // Async method: wrap Promise and convert to BrickError if (result && typeof result.then === "function") { return result.catch((error: unknown) => { // Convert to BrickError with moduleName for type-safe error handling throw BrickError.from(error, moduleName); }); } // Not wrapped: return as-is return result; } throw new Error(`Method ${methodKey} not found`); }; }, has: (_target, property) => { return typeof property === "string"; }, ownKeys: (_target) => { // Return empty array since we're proxying all property access return []; }, }); // Cache the proxy moduleCache.set(cacheKey, moduleProxy); return moduleProxy; } /** * Gets list of all registered modules * @returns Promise resolving to array of module names */ function getRegisteredModules(): string[] { const nativeModuleInstance = getNativeModule(); return nativeModuleInstance?.getRegisteredModules() ?? []; } /** * Clears the module cache (useful for testing or hot reloading) * @internal */ function clearCache(): void { moduleCache.clear(); } /** * Main Brick Module API object * Provides type-safe access to native modules */ export const BrickModule = { get, getRegisteredModules, clearCache, } as const; /** * Default export for convenience */ export default BrickModule; // Re-export types handled by main index.ts