brick-module 0.1.25 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -396,7 +396,18 @@ ${moduleNames}
396
396
  project.preBuild.doFirst {
397
397
  def workingDir = projectRoot
398
398
 
399
- // Run codegen inline
399
+ // Skip codegen if already generated to prevent deleting Gradle build outputs
400
+ // The codegen is already run during settings.gradle phase (applyBrickModules -> runBrickCodegen)
401
+ // Re-running here with --clean would delete .brick_android/build/ directory
402
+ // which contains package-aware-r.txt needed by dependent modules
403
+ def brickOutputDir = getBrickAndroidPath(workingDir)
404
+ def generatedMarker = new File(brickOutputDir, "src/main/kotlin/BrickModule.kt")
405
+ if (generatedMarker.exists()) {
406
+ // Already generated, skip to preserve build outputs
407
+ return
408
+ }
409
+
410
+ // Run codegen only if not yet generated
400
411
  try {
401
412
  def proc = ['node', '-p', "require.resolve('brick-module/package.json')"].execute(null, workingDir)
402
413
  proc.waitFor()
@@ -9,13 +9,22 @@ import com.facebook.react.bridge.ReactContext
9
9
  interface BrickModuleBase {
10
10
  /** The name of the module (required for registration) */
11
11
  val moduleName: String
12
-
12
+
13
13
  /**
14
14
  * Returns the constants exposed by this module
15
15
  * Override this method to provide module-specific constants
16
16
  * @return Map of constant name to value
17
17
  */
18
18
  fun getConstants(): Map<String, Any> = emptyMap()
19
+
20
+ /**
21
+ * Emits an event to JavaScript using React Native's auto-generated event emitters
22
+ * This method should be implemented by the bridge layer to call the appropriate
23
+ * auto-generated emitModuleName_onEventName method from React Native Codegen
24
+ * @param eventName The name of the event (e.g., "onCalculationCompleted")
25
+ * @param payload The event data as a Map
26
+ */
27
+ fun emitEvent(eventName: String, payload: Map<String, Any>)
19
28
  }
20
29
 
21
30
  /**
@@ -31,33 +40,21 @@ abstract class BrickModuleSpec(private val reactContext: ReactContext) : BrickMo
31
40
  protected fun getReactContext(): ReactContext = reactContext
32
41
 
33
42
  /**
34
- * Send an event to JavaScript This method handles event validation and routing through the
35
- * BrickHost
36
- *
37
- * @param eventName The name of the event (without module prefix)
38
- * @param data The event data to send
43
+ * Event emitter callback set by the bridge layer
44
+ * This allows the bridge to provide access to auto-generated event emitter methods
39
45
  */
40
- protected fun sendEvent(eventName: String, data: Any?) {
41
- val context = getReactContext()
46
+ var eventEmitterCallback: ((String, Map<String, Any>) -> Unit)? = null
42
47
 
43
- // Format event name with module prefix
44
- val fullEventName = "${moduleName}_$eventName"
45
-
46
- // Get the BrickHost from ReactContext
47
- val activity = context.currentActivity
48
- if (activity !is BrickModuleRegistrar) {
49
- println(
50
- " $moduleName: Current activity does not implement BrickHost, cannot send event"
48
+ /**
49
+ * Emits an event to JavaScript using React Native's auto-generated event emitters
50
+ * Delegates to the bridge layer which has access to the generated emitModuleName_onEventName methods
51
+ */
52
+ override fun emitEvent(eventName: String, payload: Map<String, Any>) {
53
+ eventEmitterCallback?.invoke(eventName, payload)
54
+ ?: throw IllegalStateException(
55
+ "Event emitter callback not set for module $moduleName. " +
56
+ "Events cannot be emitted until the module is registered with the bridge."
51
57
  )
52
- return
53
- }
54
-
55
- // Send event via BrickHost's moduleRegistry
56
- try {
57
- activity.getModuleRegistry().sendEvent(context, fullEventName, data)
58
- } catch (e: Exception) {
59
- println("❌ $moduleName: Failed to send event $fullEventName: ${e.message}")
60
- }
61
58
  }
62
59
  }
63
60
 
@@ -1,7 +1,6 @@
1
1
  package com.brickmodule
2
2
 
3
3
  import com.facebook.react.bridge.ReactContext
4
- import com.facebook.react.modules.core.DeviceEventManagerModule
5
4
  import java.util.concurrent.ConcurrentHashMap
6
5
 
7
6
  /**
@@ -73,37 +72,7 @@ class BrickModuleRegistry {
73
72
  return modules.keys.toList().sorted()
74
73
  }
75
74
 
76
- /**
77
- * Sends an event to JavaScript Used by modules to emit events to JavaScript
78
- *
79
- * @param context The ReactContext to send the event to
80
- * @param eventName The name of the event to send
81
- * @param data The event data
82
- */
83
- fun sendEvent(context: ReactContext, eventName: String, data: Any?) {
84
- try {
85
- // Ensure modules are registered
86
- if (!isRegistered) {
87
- println(
88
- "⚠️ BrickModuleRegistry: Modules not registered, skipping event: $eventName"
89
- )
90
- return
91
- }
92
-
93
- // Convert data to React Native compatible format using helper
94
- val eventData = EventDataConverter.convert(data)
95
-
96
- println("[BrickModule] sendEvent Context: $context")
97
-
98
- context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
99
- .emit(eventName, eventData)
100
-
101
- println("📡 BrickModuleRegistry: Event sent successfully: $eventName")
102
- } catch (e: Exception) {
103
- println("❌ BrickModuleRegistry: Failed to send event $eventName: ${e.message}")
104
- e.printStackTrace()
105
- }
106
- }
75
+ // No DeviceEventEmitter-based event path; events handled via RN Codegen EventEmitter
107
76
 
108
77
  /** Returns all constants from registered modules */
109
78
  fun getAllConstants(): Map<String, Any> {
@@ -7,7 +7,6 @@ import { TurboModule } from "react-native";
7
7
  */
8
8
  interface BrickModuleInterface extends TurboModule {
9
9
  readonly moduleName: string;
10
- readonly supportedEvents?: readonly string[];
11
10
  }
12
11
  /**
13
12
  * Type alias for module specifications
@@ -23,15 +22,7 @@ type BrickModuleSpec = BrickModuleInterface;
23
22
  /**
24
23
  * Enhanced typed module interface with event listeners
25
24
  */
26
- type BrickModuleWithEvents<T extends BrickModuleInterface> = T & {
27
- /**
28
- * Adds a type-safe event listener for this module
29
- * @param eventName - One of the supported events defined in the module spec
30
- * @param listener - Callback function to handle the event
31
- * @returns Unsubscription function to remove the listener
32
- */
33
- addEventListener<TEvent = unknown>(eventName: T["supportedEvents"] extends readonly (infer U)[] ? U : never, listener: (event: TEvent) => void): () => void;
34
- };
25
+ type BrickModuleWithEvents<T extends BrickModuleInterface> = T;
35
26
  /**
36
27
  * Gets a typed module instance by name with explicit type parameter
37
28
  * @param moduleName - The exact name of the module as defined in its spec
@@ -1,9 +1,8 @@
1
- import { NativeEventEmitter, TurboModuleRegistry } from "react-native";
1
+ import { TurboModuleRegistry } from "react-native";
2
2
 
3
3
  //#region src/BrickModule.ts
4
4
  const moduleCache = /* @__PURE__ */ new Map();
5
5
  let nativeModule = null;
6
- let eventEmitter = null;
7
6
  /**
8
7
  * Gets the native TurboModule instance
9
8
  * @private
@@ -12,13 +11,6 @@ function getNativeModule() {
12
11
  if (!nativeModule) nativeModule = TurboModuleRegistry.getEnforcing("BrickModule");
13
12
  return nativeModule;
14
13
  }
15
- function getEventEmitter() {
16
- if (!eventEmitter) {
17
- eventEmitter = new NativeEventEmitter(getNativeModule());
18
- console.log("eventEmitter", eventEmitter);
19
- }
20
- return eventEmitter;
21
- }
22
14
  /**
23
15
  * Gets a typed module instance by name with explicit type parameter
24
16
  * @param moduleName - The exact name of the module as defined in its spec
@@ -28,33 +20,42 @@ function get(moduleName) {
28
20
  const cacheKey = moduleName;
29
21
  if (moduleCache.has(cacheKey)) return moduleCache.get(cacheKey);
30
22
  const nativeModuleInstance = getNativeModule();
23
+ let constantsCache = null;
31
24
  const moduleProxy = new Proxy({}, {
32
25
  get: (_target, property) => {
33
26
  if (typeof property !== "string") return;
34
- if (property === "addEventListener") return (eventName, listener) => {
35
- const emitter = getEventEmitter();
36
- try {
37
- const native = getNativeModule();
38
- const moduleAddName = `${moduleName}_onEventListenerAdded`;
39
- if (typeof native[moduleAddName] === "function") native[moduleAddName](eventName);
40
- } catch {}
41
- const subscription = emitter.addListener(`${moduleName}_${eventName}`, listener);
42
- return () => {
43
- const native = getNativeModule();
44
- const moduleRemoveName = `${moduleName}_onEventListenerRemoved`;
45
- try {
46
- if (typeof native[moduleRemoveName] === "function") native[moduleRemoveName](eventName);
47
- } catch {} finally {
48
- subscription.remove();
49
- }
50
- };
27
+ if (property.startsWith("on") && property.length > 2 && property[2] === property[2].toUpperCase()) return (listener) => {
28
+ const nativeEventMethod = `${moduleName}_${property}`;
29
+ const nativeFn = nativeModuleInstance[nativeEventMethod];
30
+ if (typeof nativeFn === "function") return nativeFn(listener);
31
+ throw new Error(`${nativeEventMethod} is not available. Did you run RN codegen?`);
32
+ };
33
+ if (property === "getConstants") return () => {
34
+ if (constantsCache !== null) return constantsCache;
35
+ const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
36
+ const moduleConstants = {};
37
+ const constantPrefix = `${moduleName}_`;
38
+ for (const key in allConstants) if (key.startsWith(constantPrefix)) {
39
+ const propName = key.substring(constantPrefix.length);
40
+ moduleConstants[propName] = allConstants[key];
41
+ }
42
+ constantsCache = moduleConstants;
43
+ return moduleConstants;
51
44
  };
52
- const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
53
- const constantKey = `${moduleName}_${property}`;
54
- if (constantKey in allConstants) return allConstants[constantKey];
55
45
  return (...args) => {
56
46
  const methodKey = `${moduleName}_${property}`;
57
- if (typeof nativeModuleInstance[methodKey] === "function") return nativeModuleInstance[methodKey](...args);
47
+ if (typeof nativeModuleInstance[methodKey] === "function") {
48
+ const result = nativeModuleInstance[methodKey](...args);
49
+ if (result && typeof result === "object" && result["~sync"] === true) if (result.success === true) return result.value;
50
+ else {
51
+ const errorInfo = result.error || {};
52
+ const error = new Error(errorInfo.message || "Unknown error");
53
+ error.name = errorInfo.code || "BRICK_ERROR";
54
+ if (errorInfo.details) error.details = errorInfo.details;
55
+ throw error;
56
+ }
57
+ return result;
58
+ }
58
59
  throw new Error(`Method ${methodKey} not found`);
59
60
  };
60
61
  },
@@ -9,7 +9,9 @@ open class BrickModuleBase: NSObject {
9
9
  /// The name of the module (required for registration)
10
10
  public let moduleName: String
11
11
 
12
- public weak var eventEmitter: RCTEventEmitter?
12
+ /// The registry that manages this module (assigned during registration)
13
+ public weak var registry: BrickModuleRegistry?
14
+
13
15
  public weak var bridgeProxy: RCTBridgeProxy?
14
16
 
15
17
  /// Initialize with module name
@@ -18,21 +20,13 @@ open class BrickModuleBase: NSObject {
18
20
  super.init()
19
21
  }
20
22
 
21
- /// Sends an event with the given name and data
22
- /// Automatically prefixes with module name and uses the registry's event emitter
23
- public func sendEvent(_ eventName: String, data: Any?) {
24
- // Format event name with module prefix
25
- let fullEventName = "\(moduleName)_\(eventName)"
26
-
27
- // Get the shared event emitter instance and send event
28
- if let eventEmitter = self.eventEmitter {
29
- eventEmitter.sendEvent(withName: fullEventName, body: data)
30
- } else {
31
- let error = NSError(domain: "BrickModule", code: 500, userInfo: [
32
- NSLocalizedDescriptionKey: "Event emitter not available, cannot send event: \(fullEventName)"
33
- ])
34
- print("Error: \(error.localizedDescription)")
23
+ // Emit via Registry event map (ObjC++ injects typed handlers per event)
24
+ public func emit(_ eventName: String, payload: [String: Any]) {
25
+ guard let registry = registry else {
26
+ print("⚠️ BrickModuleBase: registry not attached; cannot emit \(moduleName).\(eventName)")
27
+ return
35
28
  }
29
+ registry.emitEvent(module: moduleName, event: eventName, payload: payload)
36
30
  }
37
31
  }
38
32
 
@@ -10,8 +10,11 @@ import React
10
10
 
11
11
  private var modules: [String: BrickModuleBase] = [:]
12
12
  private var isRegistered: Bool = false
13
- private weak var eventEmitter: RCTEventEmitter?
13
+ public weak var brickTurboModule: NSObject?
14
14
  private weak var bridgeProxy: RCTBridgeProxy?;
15
+ // Event map injected from BrickModuleImpl (already converted to Swift types)
16
+ // Nested structure: [ModuleName: [EventName: Handler]]
17
+ private var eventMap: [String: [String: ([String: Any]) -> Void]] = [:]
15
18
 
16
19
  public override init() {
17
20
  super.init()
@@ -59,8 +62,8 @@ import React
59
62
 
60
63
  // Register the module
61
64
  modules[moduleName] = module
62
- modules[moduleName]?.eventEmitter = self.eventEmitter;
63
65
  modules[moduleName]?.bridgeProxy = self.bridgeProxy;
66
+ modules[moduleName]?.registry = self
64
67
  print("📦 BrickModuleRegistry: Registered module '\(moduleName)'")
65
68
  }
66
69
 
@@ -82,33 +85,30 @@ import React
82
85
  }
83
86
  }
84
87
 
85
- /**
86
- * Returns list of registered module names
87
- */
88
- @objc public func getRegisteredModules() -> [String] {
89
- return Array(modules.keys).sorted()
88
+ // Receives event map from BrickModuleImpl (already converted to Swift types)
89
+ public func setEventMap(_ map: [String: [String: ([String: Any]) -> Void]]) {
90
+ self.eventMap = map
90
91
  }
91
-
92
- // MARK: - Event Emitter Management
93
-
94
- /**
95
- * Sets the React Native event emitter instance for event emission
96
- * This should be called during app initialization frBrickModuleom the main BrickModule
97
- */
98
- public func setEventEmitter(_ eventEmitter: Any) {
99
- self.eventEmitter = eventEmitter as? RCTEventEmitter
100
- for module in modules {
101
- module.value.eventEmitter = self.eventEmitter
92
+
93
+ // Emit event by module+event via typed block
94
+ @objc public func emitEvent(module: String, event: String, payload: [String: Any]) {
95
+ // Direct lookup without string manipulation
96
+ if let moduleEvents = eventMap[module],
97
+ let handler = moduleEvents[event] {
98
+ handler(payload)
99
+ } else {
100
+ print("⚠️ BrickModuleRegistry: No handler for module '\(module)' event '\(event)'")
102
101
  }
103
102
  }
104
103
 
105
104
  /**
106
- * Returns the React Native event emitter instance for event emission
107
- * Used by generated module code to emit events to JavaScript
105
+ * Returns list of registered module names
108
106
  */
109
- @objc public func getEventEmitter() -> RCTEventEmitter? {
110
- return eventEmitter
107
+ @objc public func getRegisteredModules() -> [String] {
108
+ return Array(modules.keys).sorted()
111
109
  }
110
+
111
+ // MARK: - Event Emitter Management removed (CodegenTypes.EventEmitter is used)
112
112
 
113
113
  /**
114
114
  * Unregisters all modules and clears their registry references
@@ -125,8 +125,3 @@ import React
125
125
  unregister()
126
126
  }
127
127
  }
128
-
129
- public class ModuleRegistry {
130
-
131
- }
132
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brick-module",
3
- "version": "0.1.25",
3
+ "version": "0.3.1",
4
4
  "description": "Better React Native native module development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,10 +34,6 @@
34
34
  "BrickModule.podspec",
35
35
  "podfile_helper.rb"
36
36
  ],
37
- "publishConfig": {
38
- "registry": "https://registry.npmjs.org",
39
- "access": "public"
40
- },
41
37
  "keywords": [
42
38
  "react-native",
43
39
  "native-module",
@@ -62,7 +58,7 @@
62
58
  "brick-codegen": "./bin/brick-codegen.js"
63
59
  },
64
60
  "dependencies": {
65
- "brick-codegen": "0.1.25"
61
+ "brick-codegen": "0.3.1"
66
62
  },
67
63
  "peerDependencies": {
68
64
  "react": ">=18.2.0",
@@ -4,14 +4,13 @@
4
4
  */
5
5
 
6
6
  import type { TurboModule } from "react-native";
7
- import { TurboModuleRegistry, NativeEventEmitter } from "react-native";
7
+ import { TurboModuleRegistry } from "react-native";
8
8
 
9
9
  /**
10
10
  * Base interface that all Brick module specs must extend
11
11
  */
12
12
  export interface BrickModuleInterface extends TurboModule {
13
13
  readonly moduleName: string;
14
- readonly supportedEvents?: readonly string[];
15
14
  }
16
15
 
17
16
  /**
@@ -29,7 +28,6 @@ export type BrickModuleSpec = BrickModuleInterface;
29
28
  // Module-level state (previously static class members)
30
29
  const moduleCache = new Map<string, any>();
31
30
  let nativeModule: any = null;
32
- let eventEmitter: NativeEventEmitter | null = null;
33
31
 
34
32
  /**
35
33
  * Gets the native TurboModule instance
@@ -42,30 +40,10 @@ function getNativeModule() {
42
40
  return nativeModule;
43
41
  }
44
42
 
45
- function getEventEmitter() {
46
- if (!eventEmitter) {
47
- const nativeModuleInstance = getNativeModule();
48
- eventEmitter = new NativeEventEmitter(nativeModuleInstance);
49
- console.log("eventEmitter", eventEmitter);
50
- }
51
- return eventEmitter;
52
- }
53
-
54
43
  /**
55
44
  * Enhanced typed module interface with event listeners
56
45
  */
57
- export type BrickModuleWithEvents<T extends BrickModuleInterface> = T & {
58
- /**
59
- * Adds a type-safe event listener for this module
60
- * @param eventName - One of the supported events defined in the module spec
61
- * @param listener - Callback function to handle the event
62
- * @returns Unsubscription function to remove the listener
63
- */
64
- addEventListener<TEvent = unknown>(
65
- eventName: T["supportedEvents"] extends readonly (infer U)[] ? U : never,
66
- listener: (event: TEvent) => void
67
- ): () => void;
68
- };
46
+ export type BrickModuleWithEvents<T extends BrickModuleInterface> = T;
69
47
 
70
48
  /**
71
49
  * Gets a typed module instance by name with explicit type parameter
@@ -83,6 +61,9 @@ function get<T extends BrickModuleInterface>(
83
61
 
84
62
  const nativeModuleInstance = getNativeModule();
85
63
 
64
+ // Cache constants for this module (computed once, reused)
65
+ let constantsCache: Record<string, any> | null = null;
66
+
86
67
  // Create a proxy that intercepts method calls and forwards them to the native module
87
68
  const moduleProxy = new Proxy({} as BrickModuleWithEvents<T>, {
88
69
  get: (_target, property: string | symbol) => {
@@ -90,57 +71,81 @@ function get<T extends BrickModuleInterface>(
90
71
  return undefined;
91
72
  }
92
73
 
93
- // Handle event listener methods
94
- if (property === "addEventListener") {
95
- return (eventName: string, listener: (event: unknown) => void) => {
96
- const emitter = getEventEmitter();
97
- // Inform native module about listener addition (module-specific)
98
- try {
99
- const native = getNativeModule();
100
- const moduleAddName = `${moduleName}_onEventListenerAdded`;
101
- if (typeof native[moduleAddName] === "function") {
102
- native[moduleAddName](eventName);
103
- }
104
- } catch {
105
- // ignore
74
+ // New-style event subscription: onXxx((event) => void) → EventSubscription
75
+ if (
76
+ property.startsWith("on") &&
77
+ property.length > 2 &&
78
+ property[2] === property[2].toUpperCase()
79
+ ) {
80
+ return (listener: (event: unknown) => void) => {
81
+ // Respect RN CodegenTypes.EventEmitter: call `${moduleName}_onXxx` and return as-is
82
+ const nativeEventMethod = `${moduleName}_${property}`;
83
+ const nativeFn = (nativeModuleInstance as any)[nativeEventMethod];
84
+ if (typeof nativeFn === "function") {
85
+ return nativeFn(listener);
106
86
  }
107
-
108
- const subscription = emitter.addListener(
109
- `${moduleName}_${eventName}`,
110
- listener
87
+ throw new Error(
88
+ `${nativeEventMethod} is not available. Did you run RN codegen?`
111
89
  );
112
-
113
- return () => {
114
- const native = getNativeModule();
115
- const moduleRemoveName = `${moduleName}_onEventListenerRemoved`;
116
- try {
117
- // Prefer module-specific native removal for precise cleanup
118
- if (typeof native[moduleRemoveName] === "function") {
119
- native[moduleRemoveName](eventName);
120
- }
121
- } catch {
122
- // ignore
123
- } finally {
124
- subscription.remove();
125
- }
126
- };
127
90
  };
128
91
  }
129
92
 
130
- // Handle individual constants - check if this property exists as a constant
131
- const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
132
- const constantKey = `${moduleName}_${property}`;
133
- if (constantKey in allConstants) {
134
- return allConstants[constantKey];
93
+ // Special handling for getConstants method (with caching)
94
+ if (property === "getConstants") {
95
+ return () => {
96
+ if (constantsCache !== null) {
97
+ return constantsCache;
98
+ }
99
+
100
+ const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
101
+ const moduleConstants: Record<string, any> = {};
102
+ const constantPrefix = `${moduleName}_`;
103
+
104
+ for (const key in allConstants) {
105
+ if (key.startsWith(constantPrefix)) {
106
+ const propName = key.substring(constantPrefix.length);
107
+ moduleConstants[propName] = allConstants[key];
108
+ }
109
+ }
110
+
111
+ constantsCache = moduleConstants;
112
+ return moduleConstants;
113
+ };
135
114
  }
136
115
 
137
116
  // Handle method calls
138
117
  return (...args: any[]) => {
139
118
  const methodKey = `${moduleName}_${property}`;
140
119
 
141
- // Try direct method call first (generated by codegen)
120
+ // Try direct method call (generated by codegen)
142
121
  if (typeof nativeModuleInstance[methodKey] === "function") {
143
- return nativeModuleInstance[methodKey](...args);
122
+ const result = nativeModuleInstance[methodKey](...args);
123
+
124
+ // Check if result is a wrapped sync method result
125
+ // Sync methods have explicit "~sync": true marker for reliable detection
126
+ // Format: { "~sync": true, success: true, value: T } | { "~sync": true, success: false, error: {...} }
127
+ if (
128
+ result &&
129
+ typeof result === "object" &&
130
+ result["~sync"] === true
131
+ ) {
132
+ if (result.success === true) {
133
+ // Success: return the unwrapped value
134
+ return result.value;
135
+ } else {
136
+ // Failure: throw an Error with details from error object
137
+ const errorInfo = result.error || {};
138
+ const error = new Error(errorInfo.message || "Unknown error");
139
+ error.name = errorInfo.code || "BRICK_ERROR";
140
+ if (errorInfo.details) {
141
+ (error as any).details = errorInfo.details;
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ // Not wrapped (async method or other): return as-is
148
+ return result;
144
149
  }
145
150
 
146
151
  throw new Error(`Method ${methodKey} not found`);