brick-module
Version:
Better React Native native module development
176 lines (151 loc) • 4.96 kB
text/typescript
/**
* Brick Module Framework - Main API
* Type-safe access to native modules with automatic code generation
*/
import type { TurboModule } from "react-native";
import { TurboModuleRegistry, NativeEventEmitter } from "react-native";
/**
* Base interface that all Brick module specs must extend
*/
export interface BrickModuleInterface extends TurboModule {
readonly moduleName: string;
readonly supportedEvents?: readonly 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;
let eventEmitter: NativeEventEmitter | null = null;
/**
* Gets the native TurboModule instance
* @private
*/
function getNativeModule() {
if (!nativeModule) {
nativeModule = TurboModuleRegistry.getEnforcing("BrickModule");
}
return nativeModule;
}
function getEventEmitter() {
if (!eventEmitter) {
const nativeModuleInstance = getNativeModule();
eventEmitter = new NativeEventEmitter(nativeModuleInstance);
console.log("eventEmitter", eventEmitter);
}
return eventEmitter;
}
/**
* Enhanced typed module interface with event listeners
*/
export type BrickModuleWithEvents<T extends BrickModuleInterface> = T & {
/**
* Adds a type-safe event listener for this module
* @param eventName - One of the supported events defined in the module spec
* @param listener - Callback function to handle the event
* @returns Unsubscription function to remove the listener
*/
addEventListener<TEvent = unknown>(
eventName: T["supportedEvents"] extends readonly (infer U)[] ? U : never,
listener: (event: TEvent) => void
): () => void;
};
/**
* 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();
// 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;
}
// Handle event listener methods
if (property === "addEventListener") {
return (eventName: string, listener: (event: unknown) => void) => {
const emitter = getEventEmitter();
const subscription = emitter.addListener(
`${moduleName}_${eventName}`,
listener
);
return () => {
subscription.remove();
};
};
}
// Handle individual constants - check if this property exists as a constant
const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
const constantKey = `${moduleName}_${property}`;
if (constantKey in allConstants) {
return allConstants[constantKey];
}
// Handle method calls
return (...args: any[]) => {
const methodKey = `${moduleName}_${property}`;
// Try direct method call first (generated by codegen)
if (typeof nativeModuleInstance[methodKey] === "function") {
return nativeModuleInstance[methodKey](...args);
}
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