expo-modules-core 57.0.2 → 57.0.4

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.
@@ -28,7 +28,8 @@ extension UIColor: Convertible {
28
28
  // Handle `PlatformColor` and `DynamicColorIOS`
29
29
  if let opaqueValue = value as? [String: Any] {
30
30
  if let semanticName = opaqueValue["semantic"] as? String,
31
- let color = resolveNamedColor(name: semanticName) {
31
+ let color = resolveNamedColor(name: semanticName)
32
+ {
32
33
  return color as! Self
33
34
  }
34
35
  if let semanticArray = opaqueValue["semantic"] as? [String] {
@@ -40,7 +41,8 @@ extension UIColor: Convertible {
40
41
  }
41
42
  if let appearances = opaqueValue["dynamic"] as? [String: Any],
42
43
  let lightColor = try appearances["light"].map({ try UIColor.convert(from: $0) }),
43
- let darkColor = try appearances["dark"].map({ try UIColor.convert(from: $0) }) {
44
+ let darkColor = try appearances["dark"].map({ try UIColor.convert(from: $0) })
45
+ {
44
46
  let highContrastLightColor = try appearances["highContrastLight"].map({ try UIColor.convert(from: $0) })
45
47
  let highContrastDarkColor = try appearances["highContrastDark"].map({ try UIColor.convert(from: $0) })
46
48
 
@@ -166,9 +168,7 @@ private func uiColorWithComponents(_ components: [Double]) throws -> UIColor {
166
168
 
167
169
  // MARK: - Color String Parsing
168
170
 
169
- /**
170
- Parses a color string (hex, rgb, hsl, hwb) into a UIColor.
171
- */
171
+ /// Parses a color string (hex, rgb, hsl, hwb) into a UIColor.
172
172
  private func colorFromString(_ value: String) throws -> UIColor {
173
173
  let input = value.trimmingCharacters(in: .whitespacesAndNewlines)
174
174
  let lowercased = input.lowercased()
@@ -233,80 +233,183 @@ private func colorFromRgba(_ rgba: UInt64) throws -> UIColor {
233
233
 
234
234
  // MARK: - CSS Color Function Regex Patterns
235
235
 
236
- nonisolated(unsafe) private let cssNumber = /[-+]?\d*\.?\d+/
237
- nonisolated(unsafe) private let cssNumberOrPercent = /[-+]?\d*\.?\d+%?/
236
+ // These are built from RegexBuilder primitives only. Composing regex-literal fragments
237
+ // (e.g. /hsla?\(\s*/) inside a builder miscompiles on some OS runtime versions
238
+ // (observed on the iOS 27.0 beta simulator) and the patterns silently stop matching.
239
+
240
+ /// A CSS number like `-12`, `+1.5`, `.5` or `100`.
241
+ nonisolated(unsafe) private let cssNumber = Regex {
242
+ Optionally {
243
+ CharacterClass.anyOf("-+")
244
+ }
245
+ ZeroOrMore(.digit)
246
+ Optionally {
247
+ "."
248
+ }
249
+ OneOrMore(.digit)
250
+ }
251
+
252
+ nonisolated(unsafe) private let cssNumberOrPercent = Regex {
253
+ cssNumber
254
+ Optionally {
255
+ "%"
256
+ }
257
+ }
258
+
259
+ nonisolated(unsafe) private let commaSeparator = Regex {
260
+ ZeroOrMore(.whitespace)
261
+ ","
262
+ ZeroOrMore(.whitespace)
263
+ }
238
264
 
239
265
  nonisolated(unsafe) private let rgbCommaRegex = Regex {
240
- /rgba?\(\s*/
241
- Capture { cssNumberOrPercent }
242
- /\s*,\s*/
243
- Capture { cssNumberOrPercent }
244
- /\s*,\s*/
245
- Capture { cssNumberOrPercent }
266
+ "rgb"
246
267
  Optionally {
247
- /\s*,\s*/
248
- Capture { cssNumberOrPercent }
268
+ "a"
269
+ }
270
+ "("
271
+ ZeroOrMore(.whitespace)
272
+ Capture {
273
+ cssNumberOrPercent
274
+ }
275
+ commaSeparator
276
+ Capture {
277
+ cssNumberOrPercent
278
+ }
279
+ commaSeparator
280
+ Capture {
281
+ cssNumberOrPercent
249
282
  }
250
- /\s*\)/
283
+ Optionally {
284
+ commaSeparator
285
+ Capture {
286
+ cssNumberOrPercent
287
+ }
288
+ }
289
+ ZeroOrMore(.whitespace)
290
+ ")"
251
291
  }.ignoresCase()
252
292
 
253
293
  nonisolated(unsafe) private let rgbSpaceRegex = Regex {
254
- /rgba?\(\s*/
255
- Capture { cssNumberOrPercent }
256
- /\s+/
257
- Capture { cssNumberOrPercent }
258
- /\s+/
259
- Capture { cssNumberOrPercent }
294
+ "rgb"
260
295
  Optionally {
261
- /\s*\/\s*/
262
- Capture { cssNumberOrPercent }
296
+ "a"
297
+ }
298
+ "("
299
+ ZeroOrMore(.whitespace)
300
+ Capture {
301
+ cssNumberOrPercent
263
302
  }
264
- /\s*\)/
303
+ OneOrMore(.whitespace)
304
+ Capture {
305
+ cssNumberOrPercent
306
+ }
307
+ OneOrMore(.whitespace)
308
+ Capture {
309
+ cssNumberOrPercent
310
+ }
311
+ Optionally {
312
+ ZeroOrMore(.whitespace)
313
+ "/"
314
+ ZeroOrMore(.whitespace)
315
+ Capture {
316
+ cssNumberOrPercent
317
+ }
318
+ }
319
+ ZeroOrMore(.whitespace)
320
+ ")"
265
321
  }.ignoresCase()
266
322
 
267
323
  nonisolated(unsafe) private let hslCommaRegex = Regex {
268
- /hsla?\(\s*/
269
- Capture { cssNumber }
270
- /\s*,\s*/
271
- Capture { cssNumber }
272
- /%\s*,\s*/
273
- Capture { cssNumber }
274
- /%\s*/
324
+ "hsl"
325
+ Optionally {
326
+ "a"
327
+ }
328
+ "("
329
+ ZeroOrMore(.whitespace)
330
+ Capture {
331
+ cssNumber
332
+ }
333
+ commaSeparator
334
+ Capture {
335
+ cssNumber
336
+ }
337
+ "%"
338
+ commaSeparator
339
+ Capture {
340
+ cssNumber
341
+ }
342
+ "%"
343
+ ZeroOrMore(.whitespace)
275
344
  Optionally {
276
- /,\s*/
277
- Capture { cssNumberOrPercent }
345
+ ","
346
+ ZeroOrMore(.whitespace)
347
+ Capture {
348
+ cssNumberOrPercent
349
+ }
278
350
  }
279
- /\s*\)/
351
+ ZeroOrMore(.whitespace)
352
+ ")"
280
353
  }.ignoresCase()
281
354
 
282
355
  nonisolated(unsafe) private let hslSpaceRegex = Regex {
283
- /hsla?\(\s*/
284
- Capture { cssNumber }
285
- /\s+/
286
- Capture { cssNumber }
287
- /%\s+/
288
- Capture { cssNumber }
289
- /%\s*/
356
+ "hsl"
290
357
  Optionally {
291
- /\/\s*/
292
- Capture { cssNumberOrPercent }
358
+ "a"
359
+ }
360
+ "("
361
+ ZeroOrMore(.whitespace)
362
+ Capture {
363
+ cssNumber
364
+ }
365
+ OneOrMore(.whitespace)
366
+ Capture {
367
+ cssNumber
368
+ }
369
+ "%"
370
+ OneOrMore(.whitespace)
371
+ Capture {
372
+ cssNumber
293
373
  }
294
- /\s*\)/
374
+ "%"
375
+ ZeroOrMore(.whitespace)
376
+ Optionally {
377
+ "/"
378
+ ZeroOrMore(.whitespace)
379
+ Capture {
380
+ cssNumberOrPercent
381
+ }
382
+ }
383
+ ZeroOrMore(.whitespace)
384
+ ")"
295
385
  }.ignoresCase()
296
386
 
297
387
  nonisolated(unsafe) private let hwbRegex = Regex {
298
- /hwb\(\s*/
299
- Capture { cssNumber }
300
- /\s+/
301
- Capture { cssNumber }
302
- /%\s+/
303
- Capture { cssNumber }
304
- /%\s*/
388
+ "hwb("
389
+ ZeroOrMore(.whitespace)
390
+ Capture {
391
+ cssNumber
392
+ }
393
+ OneOrMore(.whitespace)
394
+ Capture {
395
+ cssNumber
396
+ }
397
+ "%"
398
+ OneOrMore(.whitespace)
399
+ Capture {
400
+ cssNumber
401
+ }
402
+ "%"
403
+ ZeroOrMore(.whitespace)
305
404
  Optionally {
306
- /\/\s*/
307
- Capture { cssNumberOrPercent }
405
+ "/"
406
+ ZeroOrMore(.whitespace)
407
+ Capture {
408
+ cssNumberOrPercent
409
+ }
308
410
  }
309
- /\s*\)/
411
+ ZeroOrMore(.whitespace)
412
+ ")"
310
413
  }.ignoresCase()
311
414
 
312
415
  // MARK: - CSS Color Function Parsers
@@ -387,8 +490,7 @@ private func hwbToUIColor(h: CGFloat, w: CGFloat, b: CGFloat, a: CGFloat) -> UIC
387
490
  }
388
491
 
389
492
  private func colorFromRGBString(_ rgbString: String) throws -> UIColor {
390
- if let match = rgbString.wholeMatch(of: rgbCommaRegex) ?? rgbString.wholeMatch(of: rgbSpaceRegex)
391
- {
493
+ if let match = rgbString.wholeMatch(of: rgbCommaRegex) ?? rgbString.wholeMatch(of: rgbSpaceRegex) {
392
494
  let r = parseRgbComponent(match.1)
393
495
  let g = parseRgbComponent(match.2)
394
496
  let b = parseRgbComponent(match.3)
@@ -399,8 +501,7 @@ private func colorFromRGBString(_ rgbString: String) throws -> UIColor {
399
501
  }
400
502
 
401
503
  private func colorFromHSLString(_ hslString: String) throws -> UIColor {
402
- if let match = hslString.wholeMatch(of: hslCommaRegex) ?? hslString.wholeMatch(of: hslSpaceRegex)
403
- {
504
+ if let match = hslString.wholeMatch(of: hslCommaRegex) ?? hslString.wholeMatch(of: hslSpaceRegex) {
404
505
  let h = CGFloat(Double(match.1) ?? 0)
405
506
  let s = CGFloat(min(max((Double(match.2) ?? 0) / 100.0, 0), 1))
406
507
  let l = CGFloat(min(max((Double(match.3) ?? 0) / 100.0, 0), 1))
@@ -421,10 +522,8 @@ private func colorFromHWBString(_ hwbString: String) throws -> UIColor {
421
522
  throw InvalidHWBColorException(hwbString)
422
523
  }
423
524
 
424
- /**
425
- Color components for named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color)
426
- and additionally the transparent color.
427
- */
525
+ /// Color components for named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color)
526
+ /// and additionally the transparent color.
428
527
  private let namedColors: [String: [Double]] = [
429
528
  "aliceblue": [240, 248, 255, 255],
430
529
  "antiquewhite": [250, 235, 215, 255],
@@ -71,7 +71,7 @@ public macro Event(_ name: String? = nil, sync: Bool = false) =
71
71
  /// `expo-modules-core` calls it automatically and merges the result into the module's
72
72
  /// definition, so the user doesn't have to reference it from `definition()`. `@JS` functions are
73
73
  /// additionally bound directly into the module's JS object by a synthesized
74
- /// `_decorateModule(object:in:appContext:)`.
74
+ /// `_decorateModule(object:in:)`.
75
75
  ///
76
76
  /// The module's JavaScript name is synthesized into a `_jsName` static and read by core, so there
77
77
  /// is no `Name(…)` DSL entry. It defaults to the class name; pass `@ExpoModule("CustomName")` to
@@ -100,6 +100,12 @@ public macro ExpoModule(_ name: String? = nil, classes: [Any.Type] = []) =
100
100
  /// The companion `@ExpoModule(classes: [Foo.self])` wires the class into the module's
101
101
  /// exposed surface.
102
102
  ///
103
+ /// `@JS` methods and properties are bound directly onto the class prototype by an override of
104
+ /// `_decorateSharedObject(prototype:in:)`, and a single `@JS init(...)` becomes an override
105
+ /// of `_constructSharedObject(this:arguments:in:)` that builds the native instance from the
106
+ /// JS arguments. Core calls both when building the class, so the synthesized `Class(…)` block carries
107
+ /// no DSL entries for the `@JS` members.
108
+ ///
103
109
  /// Usage:
104
110
  ///
105
111
  /// @SharedObject
@@ -113,7 +119,10 @@ public macro ExpoModule(_ name: String? = nil, classes: [Any.Type] = []) =
113
119
  /// @JS
114
120
  /// var size: Int { 42 }
115
121
  /// }
116
- @attached(member, names: named(_synthesizedClassDefinition))
122
+ @attached(
123
+ member,
124
+ names:
125
+ named(_synthesizedClassDefinition), named(_decorateSharedObject), named(_constructSharedObject))
117
126
  @attached(memberAttribute)
118
127
  public macro SharedObject(_ name: String? = nil) =
119
128
  #externalMacro(module: "ExpoModulesMacros", type: "SharedObjectMacro")
@@ -57,15 +57,20 @@ public class ConcurrentFunctionDefinition<Args, FirstArgType, ReturnType>: AnyCo
57
57
 
58
58
  @JavaScriptActor
59
59
  func call(_ appContext: AppContext, this: JavaScriptValue, arguments: consuming JavaScriptValuesBuffer) async throws -> JavaScriptValue {
60
+ let argumentsTuple: Args
60
61
  do {
61
- try validateArgumentsNumber(function: self, received: arguments.count)
62
-
63
- // Arguments must be converted on the JS thread, before we jump to another thread.
64
- let nativeArguments = try toNativeClosureArguments(converter: appContext.converter, fn: self, this: this, arguments: arguments)
62
+ argumentsTuple = try decodeArguments(appContext: appContext, this: this, arguments: arguments)
63
+ } catch let error as Exception {
64
+ throw FunctionCallException(name).causedBy(error)
65
+ } catch {
66
+ throw UnexpectedException(error)
67
+ }
68
+ return try await call(appContext, argumentsTuple: argumentsTuple)
69
+ }
65
70
 
66
- guard let argumentsTuple: Args = try Conversions.toTuple(nativeArguments) else {
67
- throw ArgumentConversionException()
68
- }
71
+ @JavaScriptActor
72
+ private func call(_ appContext: AppContext, argumentsTuple: sending Args) async throws -> JavaScriptValue {
73
+ do {
69
74
  // Safe to mark as nonisolated(unsafe) — the tuple contains fully converted native values
70
75
  // with no references back to JS objects, so it can safely cross the actor boundary.
71
76
  nonisolated(unsafe) let nonisolatedArgumentsTuple = argumentsTuple
@@ -90,16 +95,57 @@ public class ConcurrentFunctionDefinition<Args, FirstArgType, ReturnType>: AnyCo
90
95
 
91
96
  @JavaScriptActor
92
97
  func build(appContext: AppContext) throws -> JavaScriptObject {
93
- return try appContext.runtime.createAsyncFunction(name) { [weak appContext, self] this, arguments in
98
+ return try appContext.runtime.createFunction(name) { [weak appContext, self] this, arguments in
94
99
  guard let appContext else {
95
100
  throw Exceptions.AppContextLost()
96
101
  }
97
- return try await self.call(appContext, this: this, arguments: arguments)
102
+
103
+ let runtime = try appContext.runtime
104
+ let promise = try JavaScriptPromise(runtime)
105
+ let argumentsTuple: Args
106
+
107
+ do {
108
+ argumentsTuple = try self.decodeArguments(appContext: appContext, this: this, arguments: arguments)
109
+ } catch let error as Exception {
110
+ promise.reject(FunctionCallException(self.name).causedBy(error))
111
+ return promise.asValue()
112
+ } catch {
113
+ promise.reject(UnexpectedException(error))
114
+ return promise.asValue()
115
+ }
116
+
117
+ runtime.schedule {
118
+ do {
119
+ let result = try await self.call(appContext, argumentsTuple: argumentsTuple)
120
+ promise.resolve(result)
121
+ } catch {
122
+ promise.reject(error)
123
+ }
124
+ }
125
+
126
+ return promise.asValue()
98
127
  }.asObject()
99
128
  }
100
129
 
101
130
  // MARK: - Privates
102
131
 
132
+ @JavaScriptActor
133
+ private func decodeArguments(
134
+ appContext: AppContext,
135
+ this: JavaScriptValue,
136
+ arguments: consuming JavaScriptValuesBuffer
137
+ ) throws -> Args {
138
+ try validateArgumentsNumber(function: self, received: arguments.count)
139
+
140
+ let nativeArguments = try toNativeClosureArguments(converter: appContext.converter, fn: self, this: this, arguments: arguments)
141
+
142
+ guard let argumentsTuple: Args = try Conversions.toTuple(nativeArguments) else {
143
+ throw ArgumentConversionException()
144
+ }
145
+
146
+ return argumentsTuple
147
+ }
148
+
103
149
  @MainActor
104
150
  private func callBodyOnMainActor(_ args: sending Args) async throws -> sending ReturnType {
105
151
  return try await body(args)
@@ -139,7 +139,7 @@ public final class ModuleHolder {
139
139
 
140
140
  // Install the `@JS` members the `@ExpoModule` macro binds directly into the JS object
141
141
  // (the direct-JSI path). A no-op for modules that don't use the macro.
142
- try module._decorateModule(object: object, in: appContext.runtime, appContext: appContext)
142
+ try module._decorateModule(object: object, in: appContext.runtime)
143
143
 
144
144
  return object
145
145
  } catch {
@@ -34,7 +34,7 @@ public protocol AnyModule: AnyObject, AnyArgument {
34
34
  /// host functions are installed alongside the DSL-described surface. Framework-internal
35
35
  /// (leading underscore). Modules that don't use the macro fall back to the default no-op.
36
36
  @JavaScriptActor
37
- func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws
37
+ func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws
38
38
  }
39
39
 
40
40
  public extension AnyModule {
@@ -53,7 +53,7 @@ public extension AnyModule {
53
53
  }
54
54
 
55
55
  @JavaScriptActor
56
- func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
56
+ func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
57
57
  // No-op by default — only `@ExpoModule`-macro modules synthesize a real implementation.
58
58
  }
59
59
  }
@@ -83,6 +83,20 @@ open class SharedObject: AnySharedObject {
83
83
  public func emit<P: AnyArgument>(event: String, arguments: sending P) {
84
84
  emit(event: event, payload: arguments)
85
85
  }
86
+
87
+ // MARK: - Macro-synthesized JSI hooks
88
+
89
+ /// Binds the class's `@JS` members onto its JavaScript prototype. Overridden by the `@SharedObject`
90
+ /// macro; a `class func` (not a protocol requirement) so dispatch resolves per subclass.
91
+ @JavaScriptActor
92
+ open class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {}
93
+
94
+ /// Builds a native instance from the JS constructor arguments, or `nil` to use the DSL `Constructor`
95
+ /// path. Overridden by the `@SharedObject` macro from the class's `@JS init`.
96
+ @JavaScriptActor
97
+ open class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
98
+ return nil
99
+ }
86
100
  }
87
101
 
88
102
  extension SharedObject: EventEmitter {
@@ -2,24 +2,52 @@
2
2
 
3
3
  import ExpoModulesJSI
4
4
 
5
- /**
6
- Native state subclass attached to a SharedObject's JS object. Holds the paired
7
- native `SharedObject` so callers that already have the JS object can recover
8
- the native counterpart via `JavaScriptObject.getNativeState(as:)` without going
9
- through `SharedObjectRegistry`.
10
- */
5
+ /// Native state attached to a `SharedObject`'s JS object(s). Holds the paired native `SharedObject` so
6
+ /// callers that already have the JS object can recover the native counterpart via
7
+ /// `JavaScriptObject.getNativeState(as:)` without going through `SharedObjectRegistry`.
8
+ ///
9
+ /// A single instance is shared across every runtime the native object is exposed to: its underlying C++
10
+ /// `jsi::NativeState` is attached to each runtime's JS object (the same `shared_ptr`, reused via
11
+ /// `JavaScriptNativeState.acquireShared`), and this instance maps each runtime to its paired JS object so
12
+ /// the native object can recover *its* JS counterpart in any given runtime.
11
13
  internal final class SharedObjectNativeState: JavaScriptNativeState {
12
14
  internal let native: SharedObject
13
15
 
14
- /**
15
- Weak reference to the JS object this native state is attached to. Populated
16
- right after `setNativeState` so callers can recover the JS object from the
17
- native side without going through `SharedObjectRegistry`.
18
- */
19
- internal var pairedWeakObject: JavaScriptWeakObject?
16
+ /// The JS object paired with the native object in each runtime, keyed by runtime identity and held
17
+ /// weakly. One native object can be exposed to several runtimes (e.g. the main and UI runtimes, or
18
+ /// worklet contexts), each with its own JS counterpart.
19
+ private var pairedObjects: [JavaScriptRuntime.ID: JavaScriptRef<JavaScriptWeakObject>] = [:]
20
20
 
21
21
  internal init(native: SharedObject, factory: @escaping JavaScriptNativeState.Factory) {
22
22
  self.native = native
23
23
  super.init(factory: factory)
24
24
  }
25
+
26
+ /// Records `jsObject` as the native object's JS counterpart in `runtime`. The caller is responsible
27
+ /// for having attached this native state to `jsObject` (`jsObject.setNativeState(self)`), which reuses
28
+ /// the shared C++ pointee across runtimes.
29
+ internal func setJavaScriptObject(_ jsObject: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) {
30
+ // Drop entries whose runtime or JS object has gone away before inserting, so the map doesn't
31
+ // accumulate dead keys for runtimes that paired once and were torn down.
32
+ pruneExpired()
33
+ pairedObjects[runtime.id] = jsObject.createWeak().ref()
34
+ }
35
+
36
+ /// Returns the JS object paired with the native object in `runtime`, or `nil` if there is no live
37
+ /// pairing there (none was created, or the JS object has been garbage-collected).
38
+ internal func javaScriptObject(in runtime: JavaScriptRuntime) -> JavaScriptObject? {
39
+ // Unwrap the ref first so the dictionary subscript's optional doesn't nest with `withValue`'s,
40
+ // which would form a `JavaScriptObject??` that the non-`Copyable` element can't be wrapped in.
41
+ guard let ref = pairedObjects[runtime.id] else {
42
+ return nil
43
+ }
44
+ return ref.withValue { $0?.lock() }
45
+ }
46
+
47
+ private func pruneExpired() {
48
+ pairedObjects = pairedObjects.filter { _, ref in
49
+ // Keep the entry only while its weak JS object still resolves; an empty ref is dropped.
50
+ return ref.withValue { $0?.lock() != nil } ?? false
51
+ }
52
+ }
25
53
  }