brick-module 0.1.26 → 0.4.0

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.
@@ -17,7 +17,7 @@ Pod::Spec.new do |s|
17
17
  s.public_header_files = "ios/BrickModule/Public/*.h"
18
18
  s.private_header_files = "ios/BrickModule/Internal/*.h"
19
19
 
20
- # Swift 설정
20
+ # Swift settings
21
21
  s.swift_version = "5.0"
22
22
  s.requires_arc = true
23
23
 
@@ -68,6 +68,29 @@ ext.getBrickAndroidPath = { projectRoot ->
68
68
  return new File(projectRoot, 'android/.brick').canonicalPath
69
69
  }
70
70
 
71
+ ext.getBrickAndroidAppPath = { projectRoot ->
72
+ // Read brick.json from explicit project root
73
+ try {
74
+ def brickConfigFile = new File(projectRoot, 'brick.json')
75
+ if (brickConfigFile.exists()) {
76
+ def config = new JsonSlurper().parse(brickConfigFile)
77
+ def androidAppPath = config?.output?.androidAppPath
78
+ if (androidAppPath && !androidAppPath.isEmpty()) {
79
+ if (new File(androidAppPath).isAbsolute()) {
80
+ println("[Brick] Warning: Using absolute path for Android app path is not recommended: ${androidAppPath}")
81
+ return androidAppPath
82
+ }
83
+ return new File(projectRoot, androidAppPath).canonicalPath
84
+ }
85
+ }
86
+ } catch (Exception e) {
87
+ println("[Brick] Warning: Failed to read brick.json from ${projectRoot}: ${e.message}")
88
+ }
89
+
90
+ // Default: android/app in project root
91
+ return new File(projectRoot, 'android/app').canonicalPath
92
+ }
93
+
71
94
  ext.getAllDependencies = { packageJson ->
72
95
  def deps = []
73
96
  deps.addAll(packageJson.dependencies?.keySet() ?: [])
@@ -396,7 +419,23 @@ ${moduleNames}
396
419
  project.preBuild.doFirst {
397
420
  def workingDir = projectRoot
398
421
 
399
- // Run codegen inline
422
+ // Skip codegen if already generated to prevent deleting Gradle build outputs
423
+ // The codegen is already run during settings.gradle phase (applyBrickModules -> runBrickCodegen)
424
+ // Re-running here with --clean would delete .brick_android/build/ directory
425
+ // which contains package-aware-r.txt needed by dependent modules
426
+ def brickOutputDir = getBrickAndroidPath(workingDir)
427
+ def generatedMarker = new File(brickOutputDir, "src/main/kotlin/BrickModule.kt")
428
+ def androidAppPath = getBrickAndroidAppPath(workingDir)
429
+ def jniDir = new File(androidAppPath, "build/generated/autolinking/src/main/jni")
430
+ def hasJniOutputs = new File(jniDir, "BrickModuleSpec-generated.cpp").exists() &&
431
+ new File(jniDir, "BrickModuleSpec.h").exists() &&
432
+ new File(jniDir, "CMakeLists.txt").exists()
433
+ if (generatedMarker.exists() && hasJniOutputs) {
434
+ // Already generated, skip to preserve build outputs
435
+ return
436
+ }
437
+
438
+ // Run codegen only if not yet generated
400
439
  try {
401
440
  def proc = ['node', '-p', "require.resolve('brick-module/package.json')"].execute(null, workingDir)
402
441
  proc.waitFor()
@@ -408,7 +447,11 @@ ${moduleNames}
408
447
  def codegenPath = new File(brickModuleDir, "bin/brick-codegen.js")
409
448
 
410
449
  if (codegenPath.exists()) {
411
- def codegenProc = ['node', codegenPath.absolutePath, '--platform', 'android', '--projectRoot', workingDir.absolutePath].execute(null, workingDir)
450
+ def codegenArgs = ['node', codegenPath.absolutePath, '--platform', 'android', '--projectRoot', workingDir.absolutePath]
451
+ if (generatedMarker.exists()) {
452
+ codegenArgs.add('--no-clean')
453
+ }
454
+ def codegenProc = codegenArgs.execute(null, workingDir)
412
455
  codegenProc.waitFor()
413
456
  }
414
457
  }
@@ -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,38 +40,63 @@ 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()
42
-
43
- // Format event name with module prefix
44
- val fullEventName = "${moduleName}_$eventName"
46
+ var eventEmitterCallback: ((String, Map<String, Any>) -> Unit)? = null
45
47
 
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
 
64
- /** Error types for Brick modules */
65
- open class BrickModuleError(message: String, val errorCode: String) : Exception(message) {
61
+ // MARK: - BrickError Interface
62
+
63
+ /**
64
+ * Error interface for Brick modules.
65
+ * Implement this interface to propagate all error properties to JavaScript.
66
+ * Follows the same pattern as iOS BrickError protocol.
67
+ *
68
+ * Note: message is inherited from Throwable, so it's not defined in this interface.
69
+ * Classes implementing BrickError must extend Exception/Throwable.
70
+ */
71
+ interface BrickError {
72
+ /** Error code (accessed as error.name in JS) */
73
+ val code: String
74
+
75
+ /** Additional properties (accessed as error.userInfo.xxx in JS) */
76
+ val userInfo: Map<String, Any>?
77
+ get() = null
78
+ }
79
+
80
+ /**
81
+ * Default implementation of BrickError.
82
+ * Extend this class to create custom errors.
83
+ */
84
+ open class BrickException(
85
+ override val code: String,
86
+ message: String,
87
+ override val userInfo: Map<String, Any>? = null,
88
+ cause: Throwable? = null
89
+ ) : Exception(message, cause), BrickError
90
+
91
+ /** Error types for Brick modules - implements BrickError interface */
92
+ open class BrickModuleError(
93
+ message: String,
94
+ override val code: String
95
+ ) : Exception(message), BrickError {
96
+
97
+ override val userInfo: Map<String, Any>?
98
+ get() = null
99
+
66
100
  class TypeMismatch(message: String) : BrickModuleError("Type mismatch: $message", "TYPE_ERROR")
67
101
  class ExecutionError(message: String) :
68
102
  BrickModuleError("Execution error: $message", "EXECUTION_ERROR")
@@ -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> {
@@ -0,0 +1,29 @@
1
+ //#region src/BrickError.d.ts
2
+ /**
3
+ * BrickError - Type-safe class for custom errors from native modules.
4
+ * Matches the structure of iOS/Android BrickError.
5
+ */
6
+ declare class BrickError extends Error {
7
+ /** Error code (same as error.name) */
8
+ readonly code: string;
9
+ /** Additional properties (e.g., amount, currency) */
10
+ readonly userInfo: Record<string, unknown>;
11
+ /** Name of the module where the error occurred */
12
+ readonly moduleName?: string;
13
+ constructor(message: string, code: string, userInfo?: Record<string, unknown>, moduleName?: string);
14
+ /**
15
+ * Type guard to check if an error object is a BrickError.
16
+ * @param error - The error object to check
17
+ * @returns true if the error is a BrickError
18
+ */
19
+ static isBrickError(error: unknown): error is BrickError;
20
+ /**
21
+ * Converts a general error object to a BrickError.
22
+ * @param error - The error object to convert
23
+ * @param moduleName - Name of the module where the error occurred
24
+ * @returns A BrickError instance
25
+ */
26
+ static from(error: unknown, moduleName?: string): BrickError;
27
+ }
28
+ //#endregion
29
+ export { BrickError };
@@ -0,0 +1,49 @@
1
+ //#region src/BrickError.ts
2
+ /**
3
+ * BrickError - Type-safe class for custom errors from native modules.
4
+ * Matches the structure of iOS/Android BrickError.
5
+ */
6
+ var BrickError = class BrickError extends Error {
7
+ /** Error code (same as error.name) */
8
+ code;
9
+ /** Additional properties (e.g., amount, currency) */
10
+ userInfo;
11
+ /** Name of the module where the error occurred */
12
+ moduleName;
13
+ constructor(message, code, userInfo = {}, moduleName) {
14
+ super(message);
15
+ this.name = code;
16
+ this.code = code;
17
+ this.userInfo = userInfo;
18
+ this.moduleName = moduleName;
19
+ }
20
+ /**
21
+ * Type guard to check if an error object is a BrickError.
22
+ * @param error - The error object to check
23
+ * @returns true if the error is a BrickError
24
+ */
25
+ static isBrickError(error) {
26
+ return error instanceof Error && typeof error.code === "string" && typeof error.userInfo === "object" && error.userInfo !== null;
27
+ }
28
+ /**
29
+ * Converts a general error object to a BrickError.
30
+ * @param error - The error object to convert
31
+ * @param moduleName - Name of the module where the error occurred
32
+ * @returns A BrickError instance
33
+ */
34
+ static from(error, moduleName) {
35
+ if (error instanceof BrickError) {
36
+ if (moduleName && !error.moduleName) return new BrickError(error.message, error.code, error.userInfo, moduleName);
37
+ return error;
38
+ }
39
+ if (BrickError.isBrickError(error)) return new BrickError(error.message, error.code, error.userInfo, moduleName || error.moduleName);
40
+ if (error instanceof Error) {
41
+ const anyError = error;
42
+ return new BrickError(error.message, anyError.code || anyError.name || "UNKNOWN_ERROR", anyError.userInfo || {}, moduleName || anyError.moduleName);
43
+ }
44
+ return new BrickError(String(error), "UNKNOWN_ERROR", {}, moduleName);
45
+ }
46
+ };
47
+
48
+ //#endregion
49
+ export { BrickError };
@@ -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,9 @@
1
- import { NativeEventEmitter, TurboModuleRegistry } from "react-native";
1
+ import { BrickError } from "./BrickError.js";
2
+ import { TurboModuleRegistry } from "react-native";
2
3
 
3
4
  //#region src/BrickModule.ts
4
5
  const moduleCache = /* @__PURE__ */ new Map();
5
6
  let nativeModule = null;
6
- let eventEmitter = null;
7
7
  /**
8
8
  * Gets the native TurboModule instance
9
9
  * @private
@@ -12,13 +12,6 @@ function getNativeModule() {
12
12
  if (!nativeModule) nativeModule = TurboModuleRegistry.getEnforcing("BrickModule");
13
13
  return nativeModule;
14
14
  }
15
- function getEventEmitter() {
16
- if (!eventEmitter) {
17
- eventEmitter = new NativeEventEmitter(getNativeModule());
18
- console.log("eventEmitter", eventEmitter);
19
- }
20
- return eventEmitter;
21
- }
22
15
  /**
23
16
  * Gets a typed module instance by name with explicit type parameter
24
17
  * @param moduleName - The exact name of the module as defined in its spec
@@ -28,33 +21,46 @@ function get(moduleName) {
28
21
  const cacheKey = moduleName;
29
22
  if (moduleCache.has(cacheKey)) return moduleCache.get(cacheKey);
30
23
  const nativeModuleInstance = getNativeModule();
24
+ let constantsCache = null;
31
25
  const moduleProxy = new Proxy({}, {
32
26
  get: (_target, property) => {
33
27
  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
- };
28
+ if (property.startsWith("on") && property.length > 2 && property[2] === property[2].toUpperCase()) return (listener) => {
29
+ const nativeEventMethod = `${moduleName}_${property}`;
30
+ const nativeFn = nativeModuleInstance[nativeEventMethod];
31
+ if (typeof nativeFn === "function") return nativeFn(listener);
32
+ throw new Error(`${nativeEventMethod} is not available. Did you run RN codegen?`);
33
+ };
34
+ if (property === "getConstants") return () => {
35
+ if (constantsCache !== null) return constantsCache;
36
+ const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
37
+ const moduleConstants = {};
38
+ const constantPrefix = `${moduleName}_`;
39
+ for (const key in allConstants) if (key.startsWith(constantPrefix)) {
40
+ const propName = key.substring(constantPrefix.length);
41
+ moduleConstants[propName] = allConstants[key];
42
+ }
43
+ constantsCache = moduleConstants;
44
+ return moduleConstants;
51
45
  };
52
- const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
53
- const constantKey = `${moduleName}_${property}`;
54
- if (constantKey in allConstants) return allConstants[constantKey];
55
46
  return (...args) => {
56
47
  const methodKey = `${moduleName}_${property}`;
57
- if (typeof nativeModuleInstance[methodKey] === "function") return nativeModuleInstance[methodKey](...args);
48
+ if (typeof nativeModuleInstance[methodKey] === "function") {
49
+ const result = nativeModuleInstance[methodKey](...args);
50
+ if (result && typeof result === "object" && result["~sync"] === true) if (result.success === true) return result.value;
51
+ else {
52
+ const errorInfo = result.error || {};
53
+ const message = errorInfo.message || errorInfo.errorMessage || "Unknown error";
54
+ const code = errorInfo.code || errorInfo.errorCode || "BRICK_ERROR";
55
+ const userInfo = { code };
56
+ for (const key of Object.keys(errorInfo)) if (key !== "code" && key !== "message" && key !== "errorCode" && key !== "errorMessage") userInfo[key] = errorInfo[key];
57
+ throw new BrickError(message, code, userInfo, moduleName);
58
+ }
59
+ if (result && typeof result.then === "function") return result.catch((error) => {
60
+ throw BrickError.from(error, moduleName);
61
+ });
62
+ return result;
63
+ }
58
64
  throw new Error(`Method ${methodKey} not found`);
59
65
  };
60
66
  },
package/dist/index.d.ts CHANGED
@@ -1,17 +1,9 @@
1
1
  import { BrickModule, BrickModuleInterface, BrickModuleSpec } from "./BrickModule.js";
2
+ import { BrickError } from "./BrickError.js";
2
3
  import { Any, AnyObject } from "./types.js";
3
4
 
4
5
  //#region src/index.d.ts
5
6
 
6
- /**
7
- * Error types for Brick modules
8
- */
9
- declare class BrickModuleError extends Error {
10
- code: string;
11
- moduleName?: string | undefined;
12
- methodName?: string | undefined;
13
- constructor(message: string, code?: string, moduleName?: string | undefined, methodName?: string | undefined);
14
- }
15
7
  /**
16
8
  * Configuration interface for brick-codegen
17
9
  */
@@ -29,4 +21,4 @@ interface BrickCodegenConfig {
29
21
  dev?: boolean;
30
22
  }
31
23
  //#endregion
32
- export { Any, AnyObject, BrickCodegenConfig, BrickModule, BrickModuleError, type BrickModuleInterface, type BrickModuleSpec };
24
+ export { Any, AnyObject, BrickCodegenConfig, BrickError, BrickModule, type BrickModuleInterface, type BrickModuleSpec };
package/dist/index.js CHANGED
@@ -1,18 +1,4 @@
1
+ import { BrickError } from "./BrickError.js";
1
2
  import BrickModule_default from "./BrickModule.js";
2
3
 
3
- //#region src/index.ts
4
- /**
5
- * Error types for Brick modules
6
- */
7
- var BrickModuleError = class extends Error {
8
- constructor(message, code = "BRICK_ERROR", moduleName, methodName) {
9
- super(message);
10
- this.code = code;
11
- this.moduleName = moduleName;
12
- this.methodName = methodName;
13
- this.name = "BrickModuleError";
14
- }
15
- };
16
-
17
- //#endregion
18
- export { BrickModule_default as BrickModule, BrickModuleError };
4
+ export { BrickError, BrickModule_default as BrickModule };
@@ -1,6 +1,48 @@
1
1
  import Foundation
2
2
  import React
3
3
 
4
+ // MARK: - BrickError Protocol
5
+
6
+ /// Error protocol for Brick modules.
7
+ /// Conforming to this protocol propagates all error properties to JavaScript.
8
+ public protocol BrickError: Error {
9
+ /// Error message (accessed as error.message in JS)
10
+ var message: String { get }
11
+
12
+ /// Error code (accessed as error.code in JS, optional)
13
+ var code: String? { get }
14
+
15
+ /// Additional properties (accessed as error.userInfo.xxx in JS)
16
+ var userInfo: [String: Any]? { get }
17
+
18
+ /// Convert to NSError (for React Native reject)
19
+ func asNSError() -> NSError
20
+ }
21
+
22
+ public extension BrickError {
23
+ /// Default code implementation
24
+ var code: String? { nil }
25
+
26
+ /// Default userInfo implementation
27
+ var userInfo: [String: Any]? { nil }
28
+
29
+ /// Default NSError conversion implementation
30
+ func asNSError() -> NSError {
31
+ var info: [String: Any] = userInfo ?? [:]
32
+ info["code"] = code ?? "EXECUTION_ERROR"
33
+ info["message"] = message
34
+ info[NSLocalizedDescriptionKey] = message
35
+ return NSError(domain: "BrickModule", code: 0, userInfo: info)
36
+ }
37
+
38
+ /// Error code to use when calling reject
39
+ var rejectCode: String {
40
+ code ?? "EXECUTION_ERROR"
41
+ }
42
+ }
43
+
44
+ // MARK: - BrickModuleBase
45
+
4
46
  /**
5
47
  * Base class for all Brick modules
6
48
  * Provides common functionality including event emission
@@ -9,7 +51,9 @@ open class BrickModuleBase: NSObject {
9
51
  /// The name of the module (required for registration)
10
52
  public let moduleName: String
11
53
 
12
- public weak var eventEmitter: RCTEventEmitter?
54
+ /// The registry that manages this module (assigned during registration)
55
+ public weak var registry: BrickModuleRegistry?
56
+
13
57
  public weak var bridgeProxy: RCTBridgeProxy?
14
58
 
15
59
  /// Initialize with module name
@@ -18,61 +62,45 @@ open class BrickModuleBase: NSObject {
18
62
  super.init()
19
63
  }
20
64
 
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)")
65
+ // Emit via Registry event map (ObjC++ injects typed handlers per event)
66
+ public func emit(_ eventName: String, payload: [String: Any]) {
67
+ guard let registry = registry else {
68
+ print("⚠️ BrickModuleBase: registry not attached; cannot emit \(moduleName).\(eventName)")
69
+ return
35
70
  }
71
+ registry.emitEvent(module: moduleName, event: eventName, payload: payload)
36
72
  }
37
73
  }
38
74
 
75
+ // MARK: - BrickModuleError
76
+
39
77
  /**
40
78
  * Error types for Brick modules
41
79
  */
42
- public enum BrickModuleError: Error, LocalizedError {
80
+ public enum BrickModuleError: BrickError {
43
81
  case typeMismatch(String)
44
82
  case executionError(String)
45
83
  case invalidDefinition(String)
46
84
  case methodNotFound(String)
47
85
  case moduleNotFound(String)
48
-
49
- public var errorDescription: String? {
86
+
87
+ public var code: String? {
50
88
  switch self {
51
- case .typeMismatch(let message):
52
- return "Type mismatch: \(message)"
53
- case .executionError(let message):
54
- return "Execution error: \(message)"
55
- case .invalidDefinition(let message):
56
- return "Invalid definition: \(message)"
57
- case .methodNotFound(let message):
58
- return "Method not found: \(message)"
59
- case .moduleNotFound(let message):
60
- return "Module not found: \(message)"
89
+ case .typeMismatch: return "TYPE_ERROR"
90
+ case .executionError: return "EXECUTION_ERROR"
91
+ case .invalidDefinition: return "DEFINITION_ERROR"
92
+ case .methodNotFound: return "METHOD_NOT_FOUND"
93
+ case .moduleNotFound: return "MODULE_NOT_FOUND"
61
94
  }
62
95
  }
63
-
64
- public var errorCode: String {
96
+
97
+ public var message: String {
65
98
  switch self {
66
- case .typeMismatch:
67
- return "TYPE_ERROR"
68
- case .executionError:
69
- return "EXECUTION_ERROR"
70
- case .invalidDefinition:
71
- return "DEFINITION_ERROR"
72
- case .methodNotFound:
73
- return "METHOD_NOT_FOUND"
74
- case .moduleNotFound:
75
- return "MODULE_NOT_FOUND"
99
+ case .typeMismatch(let msg): return "Type mismatch: \(msg)"
100
+ case .executionError(let msg): return "Execution error: \(msg)"
101
+ case .invalidDefinition(let msg): return "Invalid definition: \(msg)"
102
+ case .methodNotFound(let msg): return "Method not found: \(msg)"
103
+ case .moduleNotFound(let msg): return "Module not found: \(msg)"
76
104
  }
77
105
  }
78
106
  }
@@ -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.26",
3
+ "version": "0.4.0",
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.26"
61
+ "brick-codegen": "0.4.0"
66
62
  },
67
63
  "peerDependencies": {
68
64
  "react": ">=18.2.0",
package/podfile_helper.rb CHANGED
@@ -27,7 +27,14 @@ def use_brick_modules!(app_path: nil)
27
27
  else
28
28
  File.expand_path(File.join(brick_root, ios_brick_path))
29
29
  end
30
- brick_codegen_podspec_path = File.join(brick_codegen_pod_path, 'BrickCodegen.podspec')
30
+ brick_codegen_podspec_path = File.join(
31
+ brick_codegen_pod_path,
32
+ 'BrickCodegen.podspec'
33
+ )
34
+ brick_codegen_pod_relative_path = relative_pod_path(
35
+ brick_codegen_pod_path,
36
+ podfile_dir
37
+ )
31
38
 
32
39
  # Run brick-codegen with real-time output and colors (iOS only)
33
40
  exit_status = system("cd #{brick_root} && FORCE_COLOR=1 npx brick-codegen --platform ios --projectRoot \"#{brick_root}\"")
@@ -42,8 +49,8 @@ def use_brick_modules!(app_path: nil)
42
49
  end
43
50
 
44
51
  # Link generated BrickCodegen pod
45
- pod 'BrickCodegen', :path => brick_codegen_pod_path
46
- Pod::UI.puts "[Brick] Linked BrickCodegen from #{brick_codegen_pod_path}"
52
+ pod 'BrickCodegen', :path => brick_codegen_pod_relative_path
53
+ Pod::UI.puts "[Brick] Linked BrickCodegen from #{brick_codegen_pod_relative_path}"
47
54
  rescue => e
48
55
  # Re-raise so CocoaPods fails the install
49
56
  raise e
@@ -74,3 +81,19 @@ def get_brick_ios_path(project_root)
74
81
 
75
82
  return File.expand_path(File.join(project_root, 'ios/.brick'))
76
83
  end
84
+
85
+ def podfile_dir
86
+ podfile_path = Pod::Config.instance.podfile_path
87
+ return Pathname.new(Dir.pwd).expand_path if podfile_path.nil?
88
+
89
+ podfile_path.dirname.expand_path
90
+ end
91
+
92
+ def relative_pod_path(target_path, base_dir)
93
+ Pathname.new(target_path)
94
+ .expand_path
95
+ .relative_path_from(base_dir)
96
+ .to_s
97
+ rescue ArgumentError
98
+ target_path
99
+ end
@@ -0,0 +1,78 @@
1
+ /**
2
+ * BrickError - Type-safe class for custom errors from native modules.
3
+ * Matches the structure of iOS/Android BrickError.
4
+ */
5
+ export class BrickError extends Error {
6
+ /** Error code (same as error.name) */
7
+ readonly code: string;
8
+
9
+ /** Additional properties (e.g., amount, currency) */
10
+ readonly userInfo: Record<string, unknown>;
11
+
12
+ /** Name of the module where the error occurred */
13
+ readonly moduleName?: string;
14
+
15
+ constructor(
16
+ message: string,
17
+ code: string,
18
+ userInfo: Record<string, unknown> = {},
19
+ moduleName?: string
20
+ ) {
21
+ super(message);
22
+ this.name = code;
23
+ this.code = code;
24
+ this.userInfo = userInfo;
25
+ this.moduleName = moduleName;
26
+ }
27
+
28
+ /**
29
+ * Type guard to check if an error object is a BrickError.
30
+ * @param error - The error object to check
31
+ * @returns true if the error is a BrickError
32
+ */
33
+ static isBrickError(error: unknown): error is BrickError {
34
+ return (
35
+ error instanceof Error &&
36
+ typeof (error as any).code === "string" &&
37
+ typeof (error as any).userInfo === "object" &&
38
+ (error as any).userInfo !== null
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Converts a general error object to a BrickError.
44
+ * @param error - The error object to convert
45
+ * @param moduleName - Name of the module where the error occurred
46
+ * @returns A BrickError instance
47
+ */
48
+ static from(error: unknown, moduleName?: string): BrickError {
49
+ if (error instanceof BrickError) {
50
+ // Update moduleName if provided
51
+ if (moduleName && !error.moduleName) {
52
+ return new BrickError(error.message, error.code, error.userInfo, moduleName);
53
+ }
54
+ return error;
55
+ }
56
+
57
+ if (BrickError.isBrickError(error)) {
58
+ return new BrickError(
59
+ error.message,
60
+ error.code,
61
+ error.userInfo,
62
+ moduleName || (error as any).moduleName
63
+ );
64
+ }
65
+
66
+ if (error instanceof Error) {
67
+ const anyError = error as any;
68
+ return new BrickError(
69
+ error.message,
70
+ anyError.code || anyError.name || "UNKNOWN_ERROR",
71
+ anyError.userInfo || {},
72
+ moduleName || anyError.moduleName
73
+ );
74
+ }
75
+
76
+ return new BrickError(String(error), "UNKNOWN_ERROR", {}, moduleName);
77
+ }
78
+ }
@@ -4,14 +4,14 @@
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
+ import { BrickError } from "./BrickError";
8
9
 
9
10
  /**
10
11
  * Base interface that all Brick module specs must extend
11
12
  */
12
13
  export interface BrickModuleInterface extends TurboModule {
13
14
  readonly moduleName: string;
14
- readonly supportedEvents?: readonly string[];
15
15
  }
16
16
 
17
17
  /**
@@ -29,7 +29,6 @@ export type BrickModuleSpec = BrickModuleInterface;
29
29
  // Module-level state (previously static class members)
30
30
  const moduleCache = new Map<string, any>();
31
31
  let nativeModule: any = null;
32
- let eventEmitter: NativeEventEmitter | null = null;
33
32
 
34
33
  /**
35
34
  * Gets the native TurboModule instance
@@ -42,30 +41,10 @@ function getNativeModule() {
42
41
  return nativeModule;
43
42
  }
44
43
 
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
44
  /**
55
45
  * Enhanced typed module interface with event listeners
56
46
  */
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
- };
47
+ export type BrickModuleWithEvents<T extends BrickModuleInterface> = T;
69
48
 
70
49
  /**
71
50
  * Gets a typed module instance by name with explicit type parameter
@@ -83,6 +62,9 @@ function get<T extends BrickModuleInterface>(
83
62
 
84
63
  const nativeModuleInstance = getNativeModule();
85
64
 
65
+ // Cache constants for this module (computed once, reused)
66
+ let constantsCache: Record<string, any> | null = null;
67
+
86
68
  // Create a proxy that intercepts method calls and forwards them to the native module
87
69
  const moduleProxy = new Proxy({} as BrickModuleWithEvents<T>, {
88
70
  get: (_target, property: string | symbol) => {
@@ -90,57 +72,95 @@ function get<T extends BrickModuleInterface>(
90
72
  return undefined;
91
73
  }
92
74
 
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
75
+ // New-style event subscription: onXxx((event) => void) → EventSubscription
76
+ if (
77
+ property.startsWith("on") &&
78
+ property.length > 2 &&
79
+ property[2] === property[2].toUpperCase()
80
+ ) {
81
+ return (listener: (event: unknown) => void) => {
82
+ // Respect RN CodegenTypes.EventEmitter: call `${moduleName}_onXxx` and return as-is
83
+ const nativeEventMethod = `${moduleName}_${property}`;
84
+ const nativeFn = (nativeModuleInstance as any)[nativeEventMethod];
85
+ if (typeof nativeFn === "function") {
86
+ return nativeFn(listener);
106
87
  }
107
-
108
- const subscription = emitter.addListener(
109
- `${moduleName}_${eventName}`,
110
- listener
88
+ throw new Error(
89
+ `${nativeEventMethod} is not available. Did you run RN codegen?`
111
90
  );
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
91
  };
128
92
  }
129
93
 
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];
94
+ // Special handling for getConstants method (with caching)
95
+ if (property === "getConstants") {
96
+ return () => {
97
+ if (constantsCache !== null) {
98
+ return constantsCache;
99
+ }
100
+
101
+ const allConstants = nativeModuleInstance?.getConstants?.() ?? {};
102
+ const moduleConstants: Record<string, any> = {};
103
+ const constantPrefix = `${moduleName}_`;
104
+
105
+ for (const key in allConstants) {
106
+ if (key.startsWith(constantPrefix)) {
107
+ const propName = key.substring(constantPrefix.length);
108
+ moduleConstants[propName] = allConstants[key];
109
+ }
110
+ }
111
+
112
+ constantsCache = moduleConstants;
113
+ return moduleConstants;
114
+ };
135
115
  }
136
116
 
137
117
  // Handle method calls
138
118
  return (...args: any[]) => {
139
119
  const methodKey = `${moduleName}_${property}`;
140
120
 
141
- // Try direct method call first (generated by codegen)
121
+ // Try direct method call (generated by codegen)
142
122
  if (typeof nativeModuleInstance[methodKey] === "function") {
143
- return nativeModuleInstance[methodKey](...args);
123
+ const result = nativeModuleInstance[methodKey](...args);
124
+
125
+ // Check if result is a wrapped sync method result
126
+ // Sync methods have explicit "~sync": true marker for reliable detection
127
+ // Format: { "~sync": true, success: true, value: T } | { "~sync": true, success: false, error: {...} }
128
+ if (
129
+ result &&
130
+ typeof result === "object" &&
131
+ result["~sync"] === true
132
+ ) {
133
+ if (result.success === true) {
134
+ // Success: return the unwrapped value
135
+ return result.value;
136
+ } else {
137
+ // Failure: throw a BrickError with all properties from error object
138
+ const errorInfo = result.error || {};
139
+ const message = errorInfo.message || errorInfo.errorMessage || "Unknown error";
140
+ const code = errorInfo.code || errorInfo.errorCode || "BRICK_ERROR";
141
+
142
+ // Build userInfo from additional properties
143
+ const userInfo: Record<string, unknown> = { code };
144
+ for (const key of Object.keys(errorInfo)) {
145
+ if (key !== "code" && key !== "message" && key !== "errorCode" && key !== "errorMessage") {
146
+ userInfo[key] = errorInfo[key];
147
+ }
148
+ }
149
+
150
+ throw new BrickError(message, code, userInfo, moduleName);
151
+ }
152
+ }
153
+
154
+ // Async method: wrap Promise and convert to BrickError
155
+ if (result && typeof result.then === "function") {
156
+ return result.catch((error: unknown) => {
157
+ // Convert to BrickError with moduleName for type-safe error handling
158
+ throw BrickError.from(error, moduleName);
159
+ });
160
+ }
161
+
162
+ // Not wrapped: return as-is
163
+ return result;
144
164
  }
145
165
 
146
166
  throw new Error(`Method ${methodKey} not found`);
package/src/index.ts CHANGED
@@ -4,20 +4,7 @@
4
4
  export type { BrickModuleInterface, BrickModuleSpec } from "./BrickModule";
5
5
  // Main API exports
6
6
  export { default as BrickModule } from "./BrickModule";
7
- /**
8
- * Error types for Brick modules
9
- */
10
- export class BrickModuleError extends Error {
11
- constructor(
12
- message: string,
13
- public code: string = "BRICK_ERROR",
14
- public moduleName?: string,
15
- public methodName?: string
16
- ) {
17
- super(message);
18
- this.name = "BrickModuleError";
19
- }
20
- }
7
+ export { BrickError } from "./BrickError";
21
8
 
22
9
  /**
23
10
  * Configuration interface for brick-codegen