first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A result builder that captures the ``ClassDefinition`` elements such as functions, constants and properties.
*/
@resultBuilder
public struct ClassDefinitionBuilder<OwnerType> {
public static func buildBlock(_ elements: AnyClassDefinitionElement...) -> [AnyClassDefinitionElement] {
return elements
}
/**
Default implementation without any constraints that just returns type-erased element.
*/
public static func buildExpression<ElementType: AnyClassDefinitionElement>(
_ element: ElementType
) -> AnyClassDefinitionElement {
return element
}
/**
In case the element's owner type matches builder's generic type,
we need to instruct the function to pass `this` to the closure
as the first argument and deduct it from `argumentsCount`.
*/
public static func buildExpression<ElementType: ClassDefinitionElement>(
_ element: ElementType
) -> AnyClassDefinitionElement where ElementType.OwnerType == OwnerType {
if var function = element as? AnyFunctionDefinition {
function.takesOwner = true
}
return element
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022-present 650 Industries. All rights reserved.
public protocol AnyObjectDefinitionElement: AnyDefinition {}
@resultBuilder
public struct ObjectDefinitionBuilder {
public static func buildBlock(_ elements: AnyObjectDefinitionElement...) -> [AnyObjectDefinitionElement] {
return elements
}
/**
Default implementation without any constraints that just returns type-erased element.
*/
public static func buildExpression<ElementType: AnyObjectDefinitionElement>(_ element: ElementType) -> AnyObjectDefinitionElement {
return element
}
}
extension SyncFunctionDefinition: AnyObjectDefinitionElement {}
extension AsyncFunctionDefinition: AnyObjectDefinitionElement {}
extension PropertyDefinition: AnyObjectDefinitionElement {}
extension ConstantDefinition: AnyObjectDefinitionElement {}
extension ConstantsDefinition: AnyObjectDefinitionElement {}

View File

@@ -0,0 +1,65 @@
/**
A result builder for the view elements such as prop setters or view events.
*/
@resultBuilder
public struct ViewDefinitionBuilder<ViewType: UIView> {
public static func buildBlock(_ elements: AnyViewDefinitionElement...) -> [AnyViewDefinitionElement] {
return elements
}
/**
Accepts `Events` definition element of `View`.
*/
public static func buildExpression(_ element: EventsDefinition) -> AnyViewDefinitionElement {
return element
}
/**
Accepts `ViewName` definition element of `View`.
*/
public static func buildExpression(_ element: ViewNameDefinition) -> AnyViewDefinitionElement {
return element
}
/**
Accepts `Prop` definition element and lets to skip defining the view type it's inferred from the `View` definition.
*/
public static func buildExpression<PropType: AnyArgument>(_ element: ConcreteViewProp<ViewType, PropType>) -> AnyViewDefinitionElement {
return element
}
/**
Accepts lifecycle methods (such as `OnViewDidUpdateProps`) as a definition element.
*/
public static func buildExpression(_ element: ViewLifecycleMethod<ViewType>) -> AnyViewDefinitionElement {
return element
}
/**
Accepts functions as a view definition elements.
*/
public static func buildExpression<ElementType: ViewDefinitionFunctionElement>(
_ element: ElementType
) -> AnyViewDefinitionElement {
return element
}
/**
Accepts functions that take the owner as a view definition elements.
*/
public static func buildExpression<ElementType: ViewDefinitionFunctionElement>(
_ element: ElementType
) -> AnyViewDefinitionElement where ElementType.ViewType == ViewType {
// Enforce async functions to run on the main queue
if var function = element as? AnyAsyncFunctionDefinition {
function.runOnQueue(.main)
function.takesOwner = true
}
if var function = element as? AnyConcurrentFunctionDefinition {
function.requiresMainActor = true
function.takesOwner = true
}
return element
}
}

View File

@@ -0,0 +1,40 @@
/**
Asynchronous function without arguments.
*/
public func AsyncFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> AsyncFunctionDefinition<(), Void, R> {
return AsyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Asynchronous function with one or more arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, each A: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, repeat each A) throws -> R
) -> AsyncFunctionDefinition<(A0, repeat each A), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: buildDynamicTypes(A0.self, repeat (each A).self),
closure
)
}
func buildDynamicTypes<A0: AnyArgument, each A: AnyArgument>(
_ first: A0.Type,
_ rest: repeat (each A).Type
) -> [AnyDynamicType] {
var result: [AnyDynamicType] = [~first]
for type in repeat each rest {
result.append(~type)
}
return result
}

View File

@@ -0,0 +1,51 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Class constructor without arguments.
*/
public func Constructor<R>(
@_implicitSelfCapture _ body: @escaping () throws -> R
) -> SyncFunctionDefinition<(), Void, R> {
return Function("constructor", body)
}
/**
Class constructor with one or more arguments.
*/
public func Constructor<R, A0: AnyArgument, each A: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, repeat each A) throws -> R
) -> SyncFunctionDefinition<(A0, repeat each A), A0, R> {
return Function("constructor", body)
}
/**
Creates the definition describing a JavaScript class.
*/
public func Class(
_ name: String,
@ClassDefinitionBuilder<JavaScriptObject> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: name, associatedType: JavaScriptObject.self, elements: elements())
}
/**
Creates the definition describing a JavaScript class with an associated native shared object class.
*/
public func Class<SharedObjectType: SharedObject>(
_ name: String = String(describing: SharedObjectType.self),
_ sharedObjectType: SharedObjectType.Type,
@ClassDefinitionBuilder<SharedObjectType> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: name, associatedType: SharedObjectType.self, elements: elements())
}
/**
Creates the definition describing a JavaScript class with an associated native shared object class
and with the name that is inferred from the shared object type.
*/
public func Class<SharedObjectType: SharedObject>(
_ sharedObjectType: SharedObjectType.Type,
@ClassDefinitionBuilder<SharedObjectType> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: String(describing: SharedObjectType.self), associatedType: SharedObjectType.self, elements: elements())
}

View File

@@ -0,0 +1,29 @@
/**
Concurrently-executing asynchronous function without arguments.
*/
public func AsyncFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () async throws -> R
) -> ConcurrentFunctionDefinition<(), Void, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Concurrently-executing asynchronous function with one or more arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, each A: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, repeat each A) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, repeat each A), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: buildDynamicTypes(A0.self, repeat (each A).self),
closure
)
}

View File

@@ -0,0 +1,13 @@
/**
Creates the read-only constant with the given name. The definition is no-op if you don't call `.get(_:)` on it.
*/
public func Constant<Value: AnyArgument>(_ name: String) -> ConstantDefinition<Value> {
return ConstantDefinition(name: name)
}
/**
Creates the read-only constant whose getter doesn't take the owner as an argument.
*/
public func Constant<Value: AnyArgument>(_ name: String, @_implicitSelfCapture get: @escaping () -> Value) -> ConstantDefinition<Value> {
return ConstantDefinition(name: name, getter: get)
}

View File

@@ -0,0 +1,41 @@
/**
Creates module's lifecycle listener that is called right after module initialization.
*/
public func OnCreate(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.moduleCreate, closure)
}
/**
Creates module's lifecycle listener that is called when the module is about to be deallocated.
*/
public func OnDestroy(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.moduleDestroy, closure)
}
/**
Creates module's lifecycle listener that is called when the app context owning the module is about to be deallocated.
*/
public func OnAppContextDestroys(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appContextDestroys, closure)
}
/**
Creates a listener that is called when the app is about to enter the foreground mode.
*/
public func OnAppEntersForeground(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appEntersForeground, closure)
}
/**
Creates a listener that is called when the app becomes active again.
*/
public func OnAppBecomesActive(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appBecomesActive, closure)
}
/**
Creates a listener that is called when the app enters the background mode.
*/
public func OnAppEntersBackground(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appEntersBackground, closure)
}

View File

@@ -0,0 +1,6 @@
/**
Sets the name of the module that is exported to the JavaScript world.
*/
public func Name(_ name: String) -> AnyDefinition {
return ModuleNameDefinition(name: name)
}

View File

@@ -0,0 +1,56 @@
/// This file implements factories for definitions that are allowed in any object-based definition `ObjectDefinition`.
/// So far only constants and functions belong to plain object.
// MARK: - Object
public func Object(@ObjectDefinitionBuilder @_implicitSelfCapture _ body: () -> [AnyDefinition]) -> ObjectDefinition {
return ObjectDefinition(definitions: body())
}
// MARK: - Constants
/**
Definition function setting the module's constants to export.
*/
@available(*, deprecated, message: "Use `Constant` or `Property` instead")
public func Constants(@_implicitSelfCapture _ body: @escaping () -> [String: Any?]) -> AnyDefinition {
return ConstantsDefinition(body: body)
}
/**
Definition function setting the module's constants to export.
*/
@available(*, deprecated, message: "Use `Constant` or `Property` instead")
public func Constants(@_implicitSelfCapture _ body: @autoclosure @escaping () -> [String: Any?]) -> AnyDefinition {
return ConstantsDefinition(body: body)
}
// MARK: - Events
/**
Defines event names that the object can send to JavaScript.
*/
public func Events(_ names: String...) -> EventsDefinition {
return EventsDefinition(names: names)
}
/**
Defines event names that the object can send to JavaScript.
*/
public func Events(_ names: [String]) -> EventsDefinition {
return EventsDefinition(names: names)
}
/**
Function that is invoked when the first event listener is added.
*/
public func OnStartObserving(_ event: String? = nil, @_implicitSelfCapture _ closure: @escaping () -> Void) -> EventObservingDefinition {
return EventObservingDefinition(type: .startObserving, event: event, closure)
}
/**
Function that is invoked when all event listeners are removed.
*/
public func OnStopObserving(_ event: String? = nil, @_implicitSelfCapture _ closure: @escaping () -> Void) -> EventObservingDefinition {
return EventObservingDefinition(type: .stopObserving, event: event, closure)
}

View File

@@ -0,0 +1,50 @@
/**
Creates the property with given name. The definition is basically no-op if you don't call `.get(_:)` or `.set(_:)` on it.
*/
public func Property(_ name: String) -> PropertyDefinition<Void> {
return PropertyDefinition(name: name)
}
/**
Creates the read-only property whose getter doesn't take the owner as an argument.
*/
public func Property<Value: AnyArgument>(_ name: String, @_implicitSelfCapture get: @escaping () -> Value) -> PropertyDefinition<Void> {
return PropertyDefinition(name: name, getter: get)
}
/**
Creates the read-only property whose getter takes the owner as an argument.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
@_implicitSelfCapture get: @escaping (_ this: OwnerType) -> Value
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name, getter: get)
}
/**
Creates the property that references to an immutable property of the owner object using the key path.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
_ keyPath: KeyPath<OwnerType, Value>
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name) { owner in
return owner[keyPath: keyPath]
}
}
/**
Creates the property that references to a mutable property of the owner object using the key path.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
_ keyPath: ReferenceWritableKeyPath<OwnerType, Value>
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name) { owner in
return owner[keyPath: keyPath]
}
.set { owner, newValue in
owner[keyPath: keyPath] = newValue
}
}

View File

@@ -0,0 +1,61 @@
/**
Static synchronous function without arguments.
*/
public func StaticFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> StaticSyncFunctionDefinition<(), Void, R> {
return StaticSyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
returnType: ~R.self,
closure
)
}
/**
Static synchronous function with one or more arguments.
*/
public func StaticFunction<R, A0: AnyArgument, each A: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, repeat each A) throws -> R
) -> StaticSyncFunctionDefinition<(A0, repeat each A), A0, R> {
return StaticSyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: buildDynamicTypes(A0.self, repeat (each A).self),
returnType: ~R.self,
closure
)
}
/**
Static asynchronous function without arguments.
*/
public func StaticAsyncFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> StaticAsyncFunctionDefinition<(), Void, R> {
return StaticAsyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Static asynchronous function with one or more arguments.
*/
public func StaticAsyncFunction<R, A0: AnyArgument, each A: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, repeat each A) throws -> R
) -> StaticAsyncFunctionDefinition<(A0, repeat each A), A0, R> {
return StaticAsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: buildDynamicTypes(A0.self, repeat (each A).self),
closure
)
}

View File

@@ -0,0 +1,31 @@
/**
Synchronous function without arguments.
*/
public func Function<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> SyncFunctionDefinition<(), Void, R> {
return SyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
returnType: ~R.self,
closure
)
}
/**
Synchronous function with one or more arguments.
*/
public func Function<R, A0: AnyArgument, each A: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, repeat each A) throws -> R
) -> SyncFunctionDefinition<(A0, repeat each A), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: buildDynamicTypes(A0.self, repeat (each A).self),
returnType: ~R.self,
closure
)
}

View File

@@ -0,0 +1,83 @@
/// Here we implement factories for the definitions exclusive for native views.
// Function names should start with a lowercase character, but in this one case
// we want it to be uppercase for Expo Modules DSL
// swiftlint:disable identifier_name
/**
Creates a view definition describing the native view exported to React.
*/
public func View<ViewType: UIView>(
_ viewType: ViewType.Type,
@ViewDefinitionBuilder<ViewType> _ elements: @escaping () -> [AnyViewDefinitionElement]
) -> ViewDefinition<ViewType> {
return ViewDefinition(viewType, elements: elements())
}
/**
Creates a view definition describing the native SwiftUI view exported to React.
*/
public func View<Props: ExpoSwiftUI.ViewProps, ViewType: ExpoSwiftUI.View>(
_ viewType: ViewType.Type
) -> ExpoSwiftUI.ViewDefinition<Props, ViewType> {
return ExpoSwiftUI.ViewDefinition(ViewType.self)
}
public func View<Props: ExpoSwiftUI.ViewProps, ViewType: ExpoSwiftUI.View>(
_ viewType: ViewType.Type,
@ExpoSwiftUI.ViewDefinitionBuilder<ViewType> _ elements: @escaping () -> [AnyViewDefinitionElement]
) -> ExpoSwiftUI.ViewDefinition<Props, ViewType> {
return ExpoSwiftUI.ViewDefinition(ViewType.self, elements: elements())
}
// MARK: Props
/**
Creates a view prop that defines its name and setter.
*/
public func Prop<ViewType: UIView, PropType: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ setter: @escaping @MainActor (ViewType, PropType) -> Void
) -> ConcreteViewProp<ViewType, PropType> {
return ConcreteViewProp(
name: name,
propType: ~PropType.self,
setter: setter
)
}
/**
Creates a view prop that defines its name, default value and setter.
*/
public func Prop<ViewType: UIView, PropType: AnyArgument>(
_ name: String,
_ defaultValue: PropType,
@_implicitSelfCapture _ setter: @escaping @MainActor (ViewType, PropType) -> Void
) -> ConcreteViewProp<ViewType, PropType> {
return ConcreteViewProp(
name: name,
propType: ~PropType.self,
defaultValue: defaultValue,
setter: setter
)
}
// MARK: - View lifecycle
/**
Defines the view lifecycle method that is called when the view finished updating all props.
*/
public func OnViewDidUpdateProps<ViewType: UIView>(
@_implicitSelfCapture _ closure: @escaping @MainActor (_ view: ViewType) -> Void
) -> ViewLifecycleMethod<ViewType> {
return ViewLifecycleMethod(type: .didUpdateProps, closure: closure)
}
/**
Sets the name of the view that is exported to the JavaScript world.
*/
public func ViewName(_ name: String) -> ViewNameDefinition {
return ViewNameDefinition(name: name)
}
// swiftlint:enable identifier_name

View File

@@ -0,0 +1,50 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
Base class for app delegate subscribers. Ensures the class
inherits from `UIResponder` and has `required init()` initializer.
*/
@objc(EXBaseAppDelegateSubscriber)
open class BaseExpoAppDelegateSubscriber: UIResponder {
public override required init() {
super.init()
}
#if os(macOS)
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
#endif // os(macOS)
}
/**
Typealias to `UIApplicationDelegate` protocol.
Might be useful for compatibility reasons if we decide to add more things here.
*/
@objc(EXAppDelegateSubscriberProtocol)
public protocol ExpoAppDelegateSubscriberProtocol: UIApplicationDelegate {
@objc optional func customizeRootView(_ rootView: UIView)
/**
Function that is called automatically by the `ExpoAppDelegate` once the subscriber is successfully registered.
If the subscriber is loaded from the modules provider, it is executed just before the main code of the app,
thus even before any other `UIApplicationDelegate` function. Use it if your subscriber needs to run some code as early as possible,
but keep in mind that this affects the application loading time.
*/
@objc
@MainActor
optional func subscriberDidRegister()
/**
Function that is called at the beginning of the `AppDelegate` initialization,
i.e. during the `main` function and before the application starts launching.
*/
@objc
@MainActor
optional func appDelegateWillBeginInitialization()
}
/**
Typealias merging the base class for app delegate subscribers and protocol inheritance to `UIApplicationDelegate`.
*/
public typealias ExpoAppDelegateSubscriber = BaseExpoAppDelegateSubscriber & ExpoAppDelegateSubscriberProtocol

View File

@@ -0,0 +1,510 @@
import Dispatch
import Foundation
@MainActor
@preconcurrency
public class ExpoAppDelegateSubscriberManager: NSObject {
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:willFinishLaunchingWithOptions:)))
}
// If we can't find a subscriber that implements `willFinishLaunchingWithOptions`, we will delegate the decision if we can handle the passed URL to
// the `didFinishLaunchingWithOptions` method by returning `true` here.
// You can read more about how iOS handles deep links here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application#discussion
if parsedSubscribers.isEmpty {
return true
}
return parsedSubscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, willFinishLaunchingWithOptions: launchOptions) ?? false || result
}
}
@objc
public static func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.application?(application, didFinishLaunchingWithOptions: launchOptions)
}
return true
}
#elseif os(macOS)
@objc
public static func applicationWillFinishLaunching(_ notification: Notification) {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(NSApplicationDelegate.applicationWillFinishLaunching(_:)))
}
parsedSubscribers.forEach { subscriber in
subscriber.applicationWillFinishLaunching?(notification)
}
}
@objc
public static func applicationDidFinishLaunching(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.applicationDidFinishLaunching?(notification)
}
}
// TODO: - Configuring and Discarding Scenes
#endif
// MARK: - Responding to App Life-Cycle Events
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidBecomeActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(application) }
}
@objc
public static func applicationWillResignActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(application) }
}
@objc
public static func applicationDidEnterBackground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidEnterBackground?(application) }
}
@objc
public static func applicationWillEnterForeground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillEnterForeground?(application) }
}
@objc
public static func applicationWillTerminate(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(application) }
}
#elseif os(macOS)
@objc
public static func applicationDidBecomeActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(notification) }
}
@objc
public static func applicationWillResignActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(notification) }
}
@objc
public static func applicationDidHide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidHide?(notification) }
}
@objc
public static func applicationWillUnhide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillUnhide?(notification) }
}
@objc
public static func applicationWillTerminate(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(notification) }
}
#endif
// MARK: - Responding to Environment Changes
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidReceiveMemoryWarning?(application) }
}
#endif
// TODO: - Managing App State Restoration
// MARK: - Downloading Data in the Background
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
if subs.isEmpty {
completionHandler()
return
}
var subscribersLeft = subs.count
let aggregatedHandler = {
DispatchQueue.main.async {
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler()
}
}
}
subs.forEach {
$0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Handling Remote Notification Registration
@objc
public static func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}
@objc
public static func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
if subs.isEmpty {
completionHandler(.noData)
return
}
var subscribersLeft = subs.count
var failedCount = 0
var newDataCount = 0
let aggregatedHandler = { (result: UIBackgroundFetchResult) in
DispatchQueue.main.async {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: aggregatedHandler)
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
let selector = #selector(NSApplicationDelegate.application(_:didReceiveRemoteNotification:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo)
}
}
#endif
// MARK: - Continuing User Activity and Handling Quick Actions
@objc
public static func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
return ExpoAppDelegateSubscriberRepository
.subscribers
.reduce(false) { result, subscriber in
return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
}
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let selector = #selector(UIApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var allRestorableObjects = [UIUserActivityRestoring]()
let aggregatedHandler = { (restorableObjects: [UIUserActivityRestoring]?) in
DispatchQueue.main.async {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: aggregatedHandler) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
) -> Bool {
let selector = #selector(NSApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var allRestorableObjects = [NSUserActivityRestoring]()
let aggregatedHandler = { (restorableObjects: [NSUserActivityRestoring]?) in
DispatchQueue.main.async {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: aggregatedHandler) ?? false || result
}
}
#endif
@objc
public static func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didUpdate: userActivity) }
}
@objc
public static func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach {
$0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
}
}
#if os(iOS)
@objc
public static func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performActionFor:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var result: Bool = false
if subs.isEmpty {
completionHandler(result)
return
}
let aggregatedHandler = { (succeeded: Bool) in
DispatchQueue.main.async {
result = result || succeeded
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler(result)
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, performActionFor: shortcutItem, completionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Background Fetch
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performFetchWithCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
if subs.isEmpty {
completionHandler(.noData)
return
}
var failedCount = 0
var newDataCount = 0
let aggregatedHandler = { (result: UIBackgroundFetchResult) in
DispatchQueue.main.async {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, performFetchWithCompletionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Opening a URL-Specified Resource
#if os(iOS) || os(tvOS)
@objc
public static func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return ExpoAppDelegateSubscriberRepository.subscribers.reduce(false) { result, subscriber in
return subscriber.application?(app, open: url, options: options) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(_ app: NSApplication, open urls: [URL]) {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
subscriber.application?(app, open: urls)
}
}
#endif
#if os(iOS)
/**
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
* a different orientation.
*/
@objc
public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
let universalOrientationMask = allowedOrientations(for: .unspecified)
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)))
}
// We want to create an intersection of all orientations set by subscribers.
let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
return result
}
return requestedOrientation.intersection(result)
}
return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
}
#endif
}
#if os(iOS)
private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
// For now only iPad-specific orientations are supported
let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
var mask: UIInterfaceOrientationMask = []
guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
return mask
}
for orientation in orientations {
switch orientation {
case "UIInterfaceOrientationPortrait":
mask.insert(.portrait)
case "UIInterfaceOrientationLandscapeLeft":
mask.insert(.landscapeLeft)
case "UIInterfaceOrientationLandscapeRight":
mask.insert(.landscapeRight)
case "UIInterfaceOrientationPortraitUpsideDown":
mask.insert(.portraitUpsideDown)
default:
break
}
}
return mask
}
#endif // os(iOS)

View File

@@ -0,0 +1,59 @@
// Copyright 2018-present 650 Industries. All rights reserved.
@MainActor private var _subscribers = [ExpoAppDelegateSubscriberProtocol]()
@MainActor private var _reactDelegateHandlers = [ExpoReactDelegateHandler]()
/**
Class responsible for managing access to app delegate subscribers and react delegates.
It should be used to access subscribers without depending on the `Expo` package where they are registered.
*/
@MainActor
@preconcurrency
@objc(EXExpoAppDelegateSubscriberRepository)
public class ExpoAppDelegateSubscriberRepository: NSObject {
@objc
public static var subscribers: [ExpoAppDelegateSubscriberProtocol] {
return _subscribers
}
@objc
public static var reactDelegateHandlers: [ExpoReactDelegateHandler] {
return _reactDelegateHandlers
}
@objc
public static func registerSubscribersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getAppDelegateSubscribers().forEach { subscriberType in
registerSubscriber(subscriberType.init())
}
}
@objc
public static func registerSubscriber(_ subscriber: ExpoAppDelegateSubscriberProtocol) {
if _subscribers.contains(where: { $0 === subscriber }) {
fatalError("Given app delegate subscriber `\(String(describing: subscriber))` is already registered.")
}
_subscribers.append(subscriber)
subscriber.subscriberDidRegister?()
}
@objc
public static func getSubscriber(_ name: String) -> ExpoAppDelegateSubscriberProtocol? {
return _subscribers.first { String(describing: $0) == name }
}
public static func getSubscriberOfType<Subscriber>(_ type: Subscriber.Type) -> Subscriber? {
return _subscribers.first { $0 is Subscriber } as? Subscriber
}
@objc
public static func registerReactDelegateHandlersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getReactDelegateHandlers()
.sorted { tuple1, tuple2 -> Bool in
return ModulePriorities.get(tuple1.packageName) > ModulePriorities.get(tuple2.packageName)
}
.forEach { handlerTuple in
_reactDelegateHandlers.append(handlerTuple.handler.init())
}
}
}

View File

@@ -0,0 +1,12 @@
public protocol ExpoReactNativeFactoryProtocol: AnyObject {
/**
To decouple RCTAppDelegate dependency from expo-modules-core,
expo-modules-core doesn't include the concrete `RCTReactNativeFactory` type and let the callsite to include the type
*/
func recreateRootView(
withBundleURL: URL?,
moduleName: String?,
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]?
) -> UIView
}

View File

@@ -0,0 +1,20 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
This class determines the order of `ExpoReactDelegateHandler`.
The priority is only for internal use and we maintain a pre-defined `SUPPORTED_MODULE` map.
*/
internal struct ModulePriorities {
static let SUPPORTED_MODULE = [
// {key}: {value}
// key: node package name
// value: priority value, the higher value takes precedence
"expo-screen-orientation": 10,
"expo-updates": 5
]
static func get(_ packageName: String) -> Int {
return SUPPORTED_MODULE[packageName] ?? 0
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
App code signing entitlements passed from autolinking
*/
public struct AppCodeSignEntitlements: Codable {
/**
https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups
*/
public var appGroups: [String]?
/**
Create an instance from JSON string.
If passing an invalid JSON string, it creates default empty entitlements instead.
*/
public static func from(json: String) -> AppCodeSignEntitlements {
guard let data = json.data(using: .utf8) else {
log.error("Invalid string encoding")
return AppCodeSignEntitlements()
}
do {
return try JSONDecoder().decode(AppCodeSignEntitlements.self, from: data)
} catch {
log.error("Unable to decode entitlement JSON data: \(error.localizedDescription)")
return AppCodeSignEntitlements()
}
}
}

View File

@@ -0,0 +1,608 @@
@preconcurrency import React
/**
The app context is an interface to a single Expo app.
*/
@objc(EXAppContext)
public final class AppContext: NSObject, @unchecked Sendable {
internal static func create() -> AppContext {
let appContext = AppContext()
appContext._runtime = ExpoRuntime()
return appContext
}
/**
The app context configuration.
*/
public let config: AppContextConfig
public lazy var jsLogger: Logger = {
let loggerModule = self.moduleRegistry.get(moduleWithName: JSLoggerModule.name) as? JSLoggerModule
guard let logger = loggerModule?.logger else {
log.error("Failed to get the JSLoggerModule logger. Falling back to OS logger.")
return log
}
return logger
}()
/**
The module registry for the app context.
*/
public private(set) lazy var moduleRegistry: ModuleRegistry = {
isModuleRegistryInitialized = true
return ModuleRegistry(appContext: self)
}()
/**
Whether the module registry for this app context has already been initialized.
*/
private var isModuleRegistryInitialized: Bool = false
/**
The legacy module registry with modules written in the old-fashioned way.
*/
@objc
public weak var legacyModuleRegistry: EXModuleRegistry?
@objc
public weak var legacyModulesProxy: LegacyNativeModulesProxy?
/**
React bridge of the context's app. Can be `nil` when the bridge
hasn't been propagated to the bridge modules yet (see ``ExpoBridgeModule``),
or when the app context is "bridgeless" (for example in native unit tests).
*/
@objc
public weak var reactBridge: RCTBridge?
/**
RCTHost wrapper. This is set by ``ExpoReactNativeFactory`` in `didInitializeRuntime`.
*/
private var hostWrapper: ExpoHostWrapper?
/**
Underlying JSI runtime of the running app.
*/
@objc
public var _runtime: ExpoRuntime? {
didSet {
if _runtime == nil {
JavaScriptActor.assumeIsolated {
// When the runtime is unpinned from the context (e.g. deallocated),
// we should make sure to release all JS objects from the memory.
// Otherwise the JSCRuntime asserts may fail on deallocation.
releaseRuntimeObjects()
}
} else if _runtime != oldValue {
JavaScriptActor.assumeIsolated {
// Try to install the core object automatically when the runtime changes.
try? prepareRuntime()
}
}
}
}
/**
JSI runtime of the running app.
*/
public var runtime: ExpoRuntime {
get throws {
if let runtime = _runtime {
return runtime
}
throw Exceptions.RuntimeLost()
}
}
@objc
public var _uiRuntime: WorkletRuntime? {
didSet {
if _uiRuntime != oldValue {
MainActor.assumeIsolated {
try? prepareUIRuntime()
}
}
}
}
public var uiRuntime: WorkletRuntime {
get throws {
if let uiRuntime = _uiRuntime {
return uiRuntime
}
throw Exceptions.UIRuntimeLost()
}
}
/**
The application identifier that is used to distinguish between different `RCTHost`.
It might be equal to `nil`, meaning we couldn't obtain the Id for the current app.
It shouldn't be used on the old architecture.
*/
@objc
public var appIdentifier: String? {
guard let moduleRegistry = reactBridge?.moduleRegistry else {
return nil
}
return "\(abs(ObjectIdentifier(moduleRegistry).hashValue))"
}
/**
Code signing entitlements for code signing
*/
public let appCodeSignEntitlements = AppContext.modulesProvider().getAppCodeSignEntitlements()
/**
The core module that defines the `expo` object in the global scope of Expo runtime.
*/
internal private(set) lazy var coreModule = CoreModule(appContext: self)
/**
The module holder for the core module.
*/
internal private(set) lazy var coreModuleHolder = ModuleHolder(appContext: self, module: coreModule, name: nil)
internal private(set) lazy var converter = MainValueConverter(appContext: self)
/**
Designated initializer without modules provider.
*/
public init(config: AppContextConfig? = nil) {
self.config = config ?? AppContextConfig(documentDirectory: nil, cacheDirectory: nil, appGroups: appCodeSignEntitlements.appGroups)
super.init()
self.moduleRegistry.register(module: JSLoggerModule(appContext: self), name: nil)
listenToClientAppNotifications()
}
public convenience init(legacyModulesProxy: Any, legacyModuleRegistry: Any, config: AppContextConfig? = nil) {
self.init(config: config)
self.legacyModulesProxy = legacyModulesProxy as? LegacyNativeModulesProxy
self.legacyModuleRegistry = legacyModuleRegistry as? EXModuleRegistry
}
@objc
public convenience override init() {
self.init(config: nil)
}
@objc
@discardableResult
public func useModulesProvider(_ providerName: String) -> Self {
return useModulesProvider(Self.modulesProvider(withName: providerName))
}
@discardableResult
public func useModulesProvider(_ provider: ModulesProvider) -> Self {
moduleRegistry.register(fromProvider: provider)
return self
}
// MARK: - UI
public func findView<ViewType>(withTag viewTag: Int, ofType type: ViewType.Type) -> ViewType? {
return hostWrapper?.findView(withTag: viewTag) as? ViewType
}
// MARK: - Running on specific queues
/**
Runs a code block on the JavaScript thread.
- Warning: This is deprecated, use `appContext.runtime.schedule` instead.
*/
@available(*, deprecated, renamed: "runtime.schedule")
public func executeOnJavaScriptThread(_ closure: @JavaScriptActor @escaping () -> Void) {
_runtime?.schedule(closure)
}
// MARK: - Classes
internal lazy var sharedObjectRegistry = SharedObjectRegistry(appContext: self)
/**
A registry containing references to JavaScript classes.
- ToDo: Make one registry per module, not the entire app context.
Perhaps it should be kept by the `ModuleHolder`.
*/
internal let classRegistry = ClassRegistry()
/**
Creates a new JavaScript object with the class prototype associated with the given native class.
- ToDo: Move this to `ModuleHolder` along the `classRegistry` property.
*/
internal func newObject(nativeClassId: ObjectIdentifier) throws -> JavaScriptObject? {
guard let jsClass = classRegistry.getJavaScriptClass(nativeClassId: nativeClassId) else {
throw JavaScriptClassNotFoundException()
}
let prototype = try jsClass.getProperty("prototype").asObject()
return try runtime.createObject(withPrototype: prototype)
}
// MARK: - Legacy modules
/**
Returns a legacy module implementing given protocol/interface.
*/
public func legacyModule<ModuleProtocol>(implementing moduleProtocol: Protocol) -> ModuleProtocol? {
return legacyModuleRegistry?.getModuleImplementingProtocol(moduleProtocol) as? ModuleProtocol ?? moduleRegistry.getModule(implementing: ModuleProtocol.self)
}
/**
Provides access to app's constants.
*/
public lazy var constants: EXConstantsInterface? = ConstantsProvider.shared
/**
Provides access to the file system utilities. Can be overridden if the app should use different different directories or file permissions.
For instance, Expo Go uses sandboxed environment per project where the cache and document directories must be scoped.
It's an optional type for historical reasons, for now let's keep it like this for backwards compatibility.
*/
public lazy var fileSystem: FileSystemManager? = FileSystemManager(appGroupSharedDirectories: self.config.appGroupSharedDirectories)
/**
Provides access to the permissions manager.
*/
public lazy var permissions: EXPermissionsService? = EXPermissionsService()
/**
Provides access to the image loader from legacy module registry.
*/
public var imageLoader: EXImageLoaderInterface? {
guard let loader = hostWrapper?.findModule(withName: "RCTImageLoader", lazilyLoadIfNecessary: true) as? RCTImageLoader else {
log.warn("Unable to get the RCTImageLoader module.")
return nil
}
return ImageLoader(rctImageLoader: loader)
}
/**
Provides access to the utilities (such as looking up for the current view controller).
*/
public var utilities: Utilities? = Utilities()
/**
Provides an event emitter that is compatible with the legacy interface.
- Deprecated as of Expo SDK 55. May be removed in the future releases.
*/
@available(*, deprecated, message: "Use `sendEvent` directly on the module instance instead")
public var eventEmitter: LegacyEventEmitterCompat? {
return LegacyEventEmitterCompat(appContext: self)
}
/**
Starts listening to `UIApplication` notifications.
*/
private func listenToClientAppNotifications() {
#if os(iOS) || os(tvOS)
let notifications = [
UIApplication.willEnterForegroundNotification,
UIApplication.didBecomeActiveNotification,
UIApplication.didEnterBackgroundNotification
]
#elseif os(macOS)
let notifications = [
NSApplication.willUnhideNotification,
NSApplication.didBecomeActiveNotification,
NSApplication.didHideNotification
]
#endif
notifications.forEach { name in
NotificationCenter.default.addObserver(self, selector: #selector(handleClientAppNotification(_:)), name: name, object: nil)
}
}
/**
Handles app's (`UIApplication`) lifecycle notifications and posts appropriate events to the module registry.
*/
@objc
private func handleClientAppNotification(_ notification: Notification) {
switch notification.name {
#if os(iOS) || os(tvOS)
case UIApplication.willEnterForegroundNotification:
moduleRegistry.post(event: .appEntersForeground)
case UIApplication.didBecomeActiveNotification:
moduleRegistry.post(event: .appBecomesActive)
case UIApplication.didEnterBackgroundNotification:
moduleRegistry.post(event: .appEntersBackground)
#elseif os(macOS)
case NSApplication.willUnhideNotification:
moduleRegistry.post(event: .appEntersForeground)
case NSApplication.didBecomeActiveNotification:
moduleRegistry.post(event: .appBecomesActive)
case NSApplication.didHideNotification:
moduleRegistry.post(event: .appEntersBackground)
#endif
default:
return
}
}
// MARK: - Interop with NativeModulesProxy
/**
Returns view modules wrapped by the base `ViewModuleWrapper` class.
*/
@objc
public func getViewManagers() -> [ViewModuleWrapper] {
return moduleRegistry.flatMap { holder in
holder.definition.views.map { key, viewDefinition in
ViewModuleWrapper(holder, viewDefinition, isDefaultModuleView: key == DEFAULT_MODULE_VIEW)
}
}
}
/**
Returns a bool whether the module with given name is registered in this context.
*/
@objc
public func hasModule(_ moduleName: String) -> Bool {
return moduleRegistry.has(moduleWithName: moduleName)
}
/**
Returns an array of names of the modules registered in the module registry.
*/
@objc
public func getModuleNames() -> [String] {
return moduleRegistry.getModuleNames()
}
/**
Returns a JavaScript object that represents a module with given name.
When remote debugging is enabled, this will always return `nil`.
*/
@JavaScriptActor
@objc
public func getNativeModuleObject(_ moduleName: String) -> JavaScriptObject? {
return moduleRegistry.get(moduleHolderForName: moduleName)?.javaScriptObject
}
/**
Asynchronously calls module's function with given arguments.
*/
@objc
public func callFunction(
_ functionName: String,
onModule moduleName: String,
withArgs args: [Any],
resolve: @escaping EXPromiseResolveBlock,
reject: @escaping EXPromiseRejectBlock
) {
moduleRegistry
.get(moduleHolderForName: moduleName)?
.call(function: functionName, args: args) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, error)
case .success(let value):
resolve(value)
}
}
}
@objc
public final lazy var expoModulesConfig = ModulesProxyConfig(constants: self.exportedModulesConstants(),
methodNames: self.exportedFunctionNames(),
viewManagers: self.viewManagersMetadata())
private func exportedFunctionNames() -> [String: [[String: Any]]] {
var constants = [String: [[String: Any]]]()
for holder in moduleRegistry {
constants[holder.name] = holder.definition.functions.map({ functionName, function in
return [
"name": functionName,
"argumentsCount": function.argumentsCount,
"key": functionName
]
})
}
return constants
}
private func exportedModulesConstants() -> [String: Any] {
return moduleRegistry
// prevent infinite recursion - exclude NativeProxyModule constants
.filter { $0.name != NativeModulesProxyModule.moduleName }
.reduce(into: [String: Any]()) { acc, holder in
acc[holder.name] = holder.getLegacyConstants()
}
}
private func viewManagersMetadata() -> [String: Any] {
return moduleRegistry.reduce(into: [String: Any]()) { acc, holder in
holder.definition.views.forEach { key, definition in
let name = key == DEFAULT_MODULE_VIEW ? holder.name : "\(holder.name)_\(definition.name)"
acc[name] = [
"propsNames": definition.props.map { $0.name }
]
}
}
}
// MARK: - Modules registration
/**
Registers native modules provided by generated `ExpoModulesProvider`.
*/
@objc
public func registerNativeModules() {
registerNativeModules(provider: Self.modulesProvider())
}
/**
Registers native modules provided by given provider.
*/
@objc
public func registerNativeModules(provider: ModulesProvider) {
useModulesProvider(provider)
// TODO: Make `registerNativeViews` thread-safe
if Thread.isMainThread {
MainActor.assumeIsolated {
self.registerNativeViews()
}
} else {
Task { @MainActor [weak self] in
self?.registerNativeViews()
}
}
}
/**
Registers native views defined by registered native modules.
- Note: It should stay private as `registerNativeModules` should be the only call site.
- Todo: `RCTComponentViewFactory` is thread-safe, so this function should be as well.
*/
@MainActor
private func registerNativeViews() {
for holder in moduleRegistry {
for (key, viewDefinition) in holder.definition.views {
let viewModule = ViewModuleWrapper(holder, viewDefinition, isDefaultModuleView: key == DEFAULT_MODULE_VIEW)
ExpoFabricView.registerComponent(viewModule, appContext: self)
}
}
}
// MARK: - Runtime
@JavaScriptActor
internal func prepareRuntime() throws {
let runtime = try runtime
let coreObject = runtime.createObject()
coreObject.defineProperty("__expo_app_identifier__", value: appIdentifier, options: [])
try coreModuleHolder.definition.decorate(object: coreObject, appContext: self)
// Initialize `global.expo`.
try runtime.initializeCoreObject(coreObject)
// Install `global.expo.EventEmitter`.
EXJavaScriptRuntimeManager.installEventEmitterClass(runtime)
// Install `global.expo.SharedObject`.
EXJavaScriptRuntimeManager.installSharedObjectClass(runtime) { [weak sharedObjectRegistry] objectId in
sharedObjectRegistry?.delete(objectId)
}
// Install `global.expo.SharedRef`.
EXJavaScriptRuntimeManager.installSharedRefClass(runtime)
// Install `global.expo.NativeModule`.
EXJavaScriptRuntimeManager.installNativeModuleClass(runtime)
// Install the modules host object as the `global.expo.modules`.
EXJavaScriptRuntimeManager.installExpoModulesHostObject(self)
}
@MainActor
internal func prepareUIRuntime() throws {
let uiRuntime = try uiRuntime
let coreObject = uiRuntime.createObject()
// Initialize `global.expo`.
uiRuntime.global().defineProperty(EXGlobalCoreObjectPropertyName, value: coreObject, options: .enumerable)
// Install `global.expo.EventEmitter`.
EXJavaScriptRuntimeManager.installEventEmitterClass(uiRuntime)
// Install `global.expo.NativeModule`.
EXJavaScriptRuntimeManager.installNativeModuleClass(uiRuntime)
}
/**
Unsets runtime objects that we hold for each module.
*/
@JavaScriptActor
private func releaseRuntimeObjects() {
sharedObjectRegistry.clear()
classRegistry.clear()
for module in moduleRegistry {
module.javaScriptObject = nil
}
}
// MARK: - Deallocation
/**
Cleans things up before deallocation.
*/
deinit {
NotificationCenter.default.removeObserver(self)
// Post an event to the registry only if it was already created.
// If we let it to lazy-load here, that would crash since the module registry
// has a weak reference to the app context which is being deallocated.
if isModuleRegistryInitialized {
moduleRegistry.post(event: .appContextDestroys)
}
}
@objc
public func setHostWrapper(_ wrapper: ExpoHostWrapper) {
self.hostWrapper = wrapper
}
// MARK: - Statics
/**
Returns an instance of the generated Expo modules provider.
The provider is usually generated in application's `ExpoModulesProviders` files group.
*/
@objc
public static func modulesProvider(withName providerName: String = "ExpoModulesProvider") -> ModulesProvider {
// [0] When ExpoModulesCore is built as separated framework/module,
// we should explicitly load main bundle's `ExpoModulesProvider` class.
if let bundleName = Bundle.main.infoDictionary?["CFBundleName"],
let providerClass = NSClassFromString("\(bundleName).\(providerName)") as? ModulesProvider.Type {
return providerClass.init()
}
// [1] Fallback to `ExpoModulesProvider` class from the current module.
if let providerClass = NSClassFromString(providerName) as? ModulesProvider.Type {
return providerClass.init()
}
// [2] Fallback to search for `ExpoModulesProvider` in frameworks (brownfield use case)
for bundle in Bundle.allFrameworks {
guard let bundleName = bundle.infoDictionary?["CFBundleName"] as? String else { continue }
if let providerClass = NSClassFromString("\(bundleName).\(providerName)") as? ModulesProvider.Type {
return providerClass.init()
}
}
// [3] Fallback to an empty `ModulesProvider` if `ExpoModulesProvider` was not generated
return ModulesProvider()
}
public func reloadAppAsync(_ reason: String = "Reload from appContext") {
if moduleRegistry.has(moduleWithName: "ExpoGo") {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "EXReloadActiveAppRequest"), object: nil)
} else {
DispatchQueue.main.async {
RCTTriggerReloadCommandListeners(reason)
}
}
}
}
// MARK: - Public exceptions
public class JavaScriptClassNotFoundException: Exception, @unchecked Sendable {
public override var reason: String {
"Unable to find a JavaScript class in the class registry"
}
}
// Deprecated since v1.0.0
@available(*, deprecated, renamed: "Exceptions.AppContextLost")
public typealias AppContextLostException = Exceptions.AppContextLost
// Deprecated since v1.0.0
@available(*, deprecated, renamed: "Exceptions.RuntimeLost")
public typealias RuntimeLostException = Exceptions.RuntimeLost

View File

@@ -0,0 +1,22 @@
// Copyright 2023-present 650 Industries. All rights reserved.
public struct AppContextConfig {
public let documentDirectory: URL?
public let cacheDirectory: URL?
public let appGroupSharedDirectories: [URL]
public let scoped: Bool
public init(documentDirectory: URL?, cacheDirectory: URL?, appGroups: [String]?) {
self.documentDirectory = documentDirectory ?? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
self.cacheDirectory = cacheDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
self.scoped = documentDirectory != nil ? true : false
var sharedDirectories: [URL] = []
for appGroup in appGroups ?? [] {
if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
sharedDirectories.append(directory)
}
}
self.appGroupSharedDirectories = sharedDirectories
}
}

View File

@@ -0,0 +1,120 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A protocol for classes/structs accepted as an argument of functions.
*/
public protocol AnyArgument {
nonisolated static func getDynamicType() -> AnyDynamicType
}
extension AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicRawType(innerType: Self.self)
}
}
// Extend the primitive types these may come from React Native bridge.
extension Bool: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicBoolType.shared
}
}
extension Int: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Int.self)
}
}
extension Int8: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Int8.self)
}
}
extension Int16: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Int16.self)
}
}
extension Int32: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Int32.self)
}
}
extension Int64: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Int64.self)
}
}
extension UInt: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: UInt.self)
}
}
extension UInt8: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: UInt8.self)
}
}
extension UInt16: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: UInt16.self)
}
}
extension UInt32: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: UInt32.self)
}
}
extension UInt64: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: UInt64.self)
}
}
extension Float32: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Float32.self)
}
}
extension Double: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: Double.self)
}
}
extension CGFloat: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicNumberType(numberType: CGFloat.self)
}
}
extension String: AnyArgument {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicStringType.shared
}
}
extension Optional: AnyArgument where Wrapped: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicOptionalType(wrappedType: ~Wrapped.self)
}
}
extension Dictionary: AnyArgument where Key: Hashable {
public static func getDynamicType() -> AnyDynamicType {
return DynamicDictionaryType(valueType: ~Value.self)
}
}
extension Array: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicArrayType(elementType: ~Element.self)
}
}
extension Data: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicDataType.shared
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
A protocol that allows custom classes or structs to be used as function arguments.
It requires static `convert(from:appContext:)` function that knows how to convert incoming
value of `Any` type to the type implemented by this protocol. It should throw an error
when the value is not recognized, is invalid or doesn't meet type requirements.
*/
public protocol Convertible: AnyArgument {
/**
Converts any value to the instance of its class (or struct) in the given app context.
Throws an error when given value cannot be converted.
*/
static func convert(from value: Any?, appContext: AppContext) throws -> Self
static func convertResult(_ result: Any, appContext: AppContext) throws -> Any
}
extension Convertible {
public static func getDynamicType() -> AnyDynamicType {
return DynamicConvertibleType(innerType: Self.self)
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
return result
}
}
@available(*, deprecated, renamed: "Convertible")
public typealias ConvertibleArgument = Convertible

View File

@@ -0,0 +1,158 @@
// Copyright 2018-present 650 Industries. All rights reserved.
import CoreGraphics
// Here we extend some common iOS types to implement `Convertible` protocol and
// describe how they can be converted from primitive types received from JavaScript runtime.
// This allows these types to be used as argument types of functions callable from JavaScript.
// As an example, when the `CGPoint` type is used as an argument type, its instance can be
// created from an array of two doubles or an object with `x` and `y` fields.
// MARK: - Foundation
extension URL: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let value = value as? String else {
if let url = value as? URL {
return url
}
throw Conversions.ConvertingException<URL>(value)
}
// First we try to create a URL without extra encoding, as it came.
if let url = convertToUrl(string: value) {
return url
}
// File path doesn't need to be percent-encoded.
if isFileUrlPath(value) {
return URL(fileURLWithPath: value)
}
// If we get here, the string is not the file url and may require percent-encoding characters that are not URL-safe according to RFC 3986.
if let encodedValue = percentEncodeUrlString(value), let url = convertToUrl(string: encodedValue) {
return url
}
// If it still fails to create the URL object, the string possibly contains characters that must be explicitly percent-encoded beforehand.
throw UrlContainsInvalidCharactersException()
}
}
internal class UrlContainsInvalidCharactersException: Exception {
override var reason: String {
return "Unable to create a URL object from the given string, make sure to percent-encode these characters: \(urlAllowedCharacters)"
}
}
// MARK: - CoreGraphics
extension CGPoint: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGPoint {
if let value = value as? [Double], value.count == 2 {
return CGPoint(x: value[0], y: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["x", "y"], as: Double.self)
return CGPoint(x: args[0], y: args[1])
}
throw Conversions.ConvertingException<CGPoint>(value)
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
if let value = result as? CGPoint {
return ["x": value.x, "y": value.y]
}
return result
}
}
extension CGSize: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGSize {
if let value = value as? [Double], value.count == 2 {
return CGSize(width: value[0], height: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["width", "height"], as: Double.self)
return CGSize(width: args[0], height: args[1])
}
if let size = value as? CGSize {
return size
}
throw Conversions.ConvertingException<CGSize>(value)
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
if let value = result as? CGSize {
return ["width": value.width, "height": value.height]
}
return result
}
}
extension CGVector: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGVector {
if let value = value as? [Double], value.count == 2 {
return CGVector(dx: value[0], dy: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["dx", "dy"], as: Double.self)
return CGVector(dx: args[0], dy: args[1])
}
if let vector = value as? CGVector {
return vector
}
throw Conversions.ConvertingException<CGVector>(value)
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
if let value = result as? CGVector {
return ["dx": value.dx, "dy": value.dy]
}
return result
}
}
extension CGRect: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGRect {
if let value = value as? [Double], value.count == 4 {
return CGRect(x: value[0], y: value[1], width: value[2], height: value[3])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["x", "y", "width", "height"], as: Double.self)
return CGRect(x: args[0], y: args[1], width: args[2], height: args[3])
}
if let rect = value as? CGRect {
return rect
}
throw Conversions.ConvertingException<CGRect>(value)
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
if let value = result as? CGRect {
return ["x": value.minX, "y": value.minY, "width": value.width, "height": value.height]
}
return result
}
}
extension Date: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Date {
if let value = value as? String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
guard let date = formatter.date(from: value) else {
throw Conversions.ConvertingException<Date>(value)
}
return date
}
// For converting the value from `Date.now()`
if let value = value as? Int {
return Date(timeIntervalSince1970: Double(value) / 1000.0)
}
if let date = value as? Date {
return date
}
throw Conversions.ConvertingException<Date>(value)
}
}

View File

@@ -0,0 +1,151 @@
// Copyright 2022-present 650 Industries. All rights reserved.
protocol AnyEither: AnyArgument {
/**
An initializer with the underlying type-erased value.
*/
init(_ value: Any?)
/**
An array of dynamic equivalents for generic either types.
*/
static func dynamicTypes() -> [AnyDynamicType]
/**
A dynamic either type for the current either type.
*/
static func getDynamicType() -> any AnyDynamicType
/**
The underlying type-erased value.
*/
var value: Any? { get }
}
/*
A convertible type wrapper for a value that should be either of two generic types.
*/
open class Either<FirstType, SecondType>: AnyEither, AnyArgument {
public class func getDynamicType() -> any AnyDynamicType {
return DynamicEitherType(eitherType: Either<FirstType, SecondType>.self)
}
/**
An array of dynamic equivalents for generic either types.
*/
class func dynamicTypes() -> [AnyDynamicType] {
return [~FirstType.self, ~SecondType.self]
}
/**
The underlying type-erased value.
*/
let value: Any?
required public init(_ value: Any?) {
self.value = value
}
/**
Returns a bool whether the value is of the first type.
*/
public func `is`(_ type: FirstType.Type) -> Bool {
return value is FirstType
}
/**
Returns a bool whether the value is of the second type.
*/
public func `is`(_ type: SecondType.Type) -> Bool {
return value is SecondType
}
/**
Returns the value as of the first type or `nil` if it's not of this type.
*/
public func get() -> FirstType? {
return value as? FirstType
}
/**
Returns the value as of the second type or `nil` if it's not of this type.
*/
public func get() -> SecondType? {
return value as? SecondType
}
public func `as`<ReturnType>(_ type: ReturnType.Type) throws -> ReturnType {
if let value = value as? ReturnType {
return value
}
throw Conversions.CastingException<ReturnType>(value as Any)
}
}
/*
A convertible type wrapper for a value that should be either of three generic types.
*/
open class EitherOfThree<FirstType, SecondType, ThirdType>: Either<FirstType, SecondType> {
override public class func getDynamicType() -> any AnyDynamicType {
return DynamicEitherType(eitherType: EitherOfThree<FirstType, SecondType, ThirdType>.self)
}
override class func dynamicTypes() -> [AnyDynamicType] {
return super.dynamicTypes() + [~ThirdType.self]
}
/**
Returns a bool whether the value is of the third type.
*/
public func `is`(_ type: ThirdType.Type) -> Bool {
return value is ThirdType
}
/**
Returns the value as of the third type or `nil` if it's not of this type.
*/
public func get() -> ThirdType? {
return value as? ThirdType
}
}
/*
A convertible type wrapper for a value that should be either of four generic types.
*/
open class EitherOfFour<FirstType, SecondType, ThirdType, FourthType>: EitherOfThree<FirstType, SecondType, ThirdType> {
override public class func getDynamicType() -> any AnyDynamicType {
return DynamicEitherType(eitherType: EitherOfFour<FirstType, SecondType, ThirdType, FourthType>.self)
}
override class func dynamicTypes() -> [AnyDynamicType] {
return super.dynamicTypes() + [~FourthType.self]
}
/**
Returns a bool whether the value is of the fourth type.
*/
public func `is`(_ type: FourthType.Type) -> Bool {
return value is FourthType
}
/**
Returns the value as of the fourth type or `nil` if it's not of this type.
*/
public func get() -> FourthType? {
return value as? FourthType
}
}
// MARK: - Exceptions
/**
An exception thrown when the value is of neither type.
*/
internal final class NeitherTypeException: GenericException<[AnyDynamicType]>, @unchecked Sendable {
override var reason: String {
var typeDescriptions = param.map({ $0.description })
let lastTypeDescription = typeDescriptions.removeLast()
return "Type must be either: \(typeDescriptions.joined(separator: ", ")) or \(lastTypeDescription)"
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A protocol that allows converting raw values to enum cases.
*/
public protocol Enumerable: AnyArgument, CaseIterable {
/**
Tries to create an enum case using given raw value.
May throw errors, e.g. when the raw value doesn't match any case.
*/
static func create<RawValueType>(fromRawValue rawValue: RawValueType) throws -> Self
/**
Returns an array of all raw values available in the enum.
*/
static var allRawValues: [Any] { get }
/**
Type-erased enum's raw value.
*/
var anyRawValue: Any { get }
}
@available(*, deprecated, renamed: "Enumerable")
public typealias EnumArgument = Enumerable
extension Enumerable {
public static func getDynamicType() -> AnyDynamicType {
return DynamicEnumType(innerType: Self.self)
}
}
/**
Extension for `Enumerable` that also conforms to `RawRepresentable`.
This constraint allows us to reference the associated `RawValue` type.
*/
public extension Enumerable where Self: RawRepresentable, Self: Hashable {
static func create<ArgType>(fromRawValue rawValue: ArgType) throws -> Self {
guard let rawValue = rawValue as? RawValue else {
throw EnumCastingException((type: RawValue.self, value: rawValue))
}
guard let enumCase = Self.init(rawValue: rawValue) else {
throw EnumNoSuchValueException((type: Self.self, value: rawValue))
}
return enumCase
}
var anyRawValue: Any {
rawValue
}
static var allRawValues: [Any] {
return allCases.map { $0.rawValue }
}
}
/**
An error that is thrown when the value cannot be cast to associated `RawValue`.
*/
internal class EnumCastingException: GenericException<(type: Any.Type, value: Any)> {
override var reason: String {
"Unable to cast '\(param.value)' to expected type \(param.type)"
}
}
/**
An error that is thrown when the value doesn't match any available case.
*/
internal class EnumNoSuchValueException: GenericException<(type: Enumerable.Type, value: Any)> {
var allRawValuesFormatted: String {
return param.type.allRawValues
.map { "'\($0)'" }
.joined(separator: ", ")
}
override var reason: String {
"'\(param.value)' is not present in \(param.type) enum, it must be one of: \(allRawValuesFormatted)"
}
}

View File

@@ -0,0 +1,66 @@
// Copyright 2021-present 650 Industries. All rights reserved.
protocol AnyValueOrUndefined: AnyArgument {}
public enum ValueOrUndefined<InnerType: AnyArgument>: AnyValueOrUndefined {
public static func getDynamicType() -> any AnyDynamicType {
return DynamicValueOrUndefinedType<InnerType>()
}
case undefined
case value(unwrapped: InnerType)
var optional: InnerType? {
return switch self {
// We want to produce Optional(nil) instead of nil - that's what DynamicOptionalType does
case .undefined: Any??.some(nil) as Any?? as! InnerType?
case .value(let value): value
}
}
var isUndefined: Bool {
return switch self {
case .undefined: true
default: false
}
}
// @deprecated because of the typo
var isUndefinded: Bool {
return self.isUndefined
}
}
extension ValueOrUndefined: Equatable where InnerType: Equatable {
public static func == (lhs: ValueOrUndefined, rhs: ValueOrUndefined) -> Bool {
return switch (lhs, rhs) {
case (.undefined, .undefined):
true
case (.value(let lhsValue), .value(let rhsValue)):
lhsValue == rhsValue
default:
false
}
}
}
extension ValueOrUndefined: Comparable where InnerType: Comparable {
public static func < (lhs: ValueOrUndefined, rhs: ValueOrUndefined) -> Bool {
return switch (lhs, rhs) {
case (.undefined, .undefined):
false // undefined is considered equal to another undefined
case (.undefined, _):
false
case (_, .undefined):
false
case (.value(let lhsValue), .value(let rhsValue)):
lhsValue < rhsValue
}
}
}
extension ValueOrUndefined {
public static func ?? <T>(valueOrUndefined: consuming ValueOrUndefined<T>, defaultValue: @autoclosure () -> T) -> T {
return valueOrUndefined.optional ?? defaultValue()
}
}

View File

@@ -0,0 +1,19 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A protocol for all array buffer types.
Array buffers represent a fixed-length raw binary data buffer.
*/
internal protocol AnyArrayBuffer: AnyArgument {
/**
Initializes an array buffer from the given underlying representation.
*/
init(_ backingBuffer: RawArrayBuffer)
}
// Extend the protocol to provide custom dynamic type
extension AnyArrayBuffer {
public static func getDynamicType() -> AnyDynamicType {
return DynamicArrayBufferType(innerType: Self.self)
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
The base class for any type of array buffer.
ArrayBuffer objects are used to represent a generic, fixed-length raw binary data buffer.
*/
public class ArrayBuffer: AnyArrayBuffer {
let backingBuffer: RawArrayBuffer
/**
Initializes the array buffer with the given raw array buffer.
- Parameter RawArrayBuffer: The underlying raw buffer implementation
*/
internal required init(_ RawArrayBuffer: RawArrayBuffer) {
self.backingBuffer = RawArrayBuffer
}
/**
The length of the ArrayBuffer in bytes.
Fixed at construction time and thus read only.
*/
public lazy var byteLength: Int = backingBuffer.getSize()
/**
The unsafe mutable raw pointer to the start of the array buffer.
*/
public lazy var rawPointer: UnsafeMutableRawPointer = backingBuffer.getUnsafeMutableRawPointer()
/**
Creates a copy of this ArrayBuffer with its own allocated memory.
- Returns: A new NativeArrayBuffer containing a copy of this buffer's data
*/
public func copy() -> NativeArrayBuffer {
ArrayBuffer.copy(of: self)
}
/**
Wraps this ArrayBuffer in a Data instance without performing a copy.
The returned Data object shares the same memory as this ArrayBuffer.
- Note: Swift `Data` is a copy-on-write type. Mutating the data
doesn't guarantee to modify the array buffer's underlying memory.
*/
public var data: Data {
// Get a strong reference to prevent deallocation while Data object exists
let sharedPointer = backingBuffer.memoryStrongRef()
return Data(
bytesNoCopy: rawPointer,
count: byteLength,
deallocator: .custom({ _, _ in sharedPointer?.reset() }))
}
/**
Creates an NSMutableData object that shares the same memory as this ArrayBuffer.
- Returns: An NSMutableData object backed by this ArrayBuffer's memory
*/
public func mutableData() -> NSMutableData {
// Get a strong reference to prevent deallocation while NSMutableData object exists
let sharedPointer = backingBuffer.memoryStrongRef()
return NSMutableData(
bytesNoCopy: rawPointer,
length: byteLength,
deallocator: { _, _ in sharedPointer?.reset() }
)
}
}

View File

@@ -0,0 +1,137 @@
// Copyright 2022-present 650 Industries. All rights reserved.
extension ArrayBuffer {
// MARK: - Wrap
/**
Wraps the given raw buffer pointer in an ArrayBuffer without copying data.
- Parameter data: The raw buffer to wrap
- Parameter cleanup: Closure called when the buffer is deallocated
- Returns: A new NativeArrayBuffer wrapping the data
- Throws: Exception if the buffer has no base address
*/
public static func wrap(
dataWithoutCopy data: UnsafeMutableRawBufferPointer,
cleanup: @escaping () -> Void
) throws -> NativeArrayBuffer {
guard let baseAddress = data.baseAddress else {
throw MissingBaseAddressError()
}
return NativeArrayBuffer(wrapping: baseAddress, count: data.count, cleanup: cleanup)
}
/**
Zero-copy wraps the given Data object in an ArrayBuffer.
- Warning: This bypasses Data's copy-on-write capabilities, effectively allowing
mutation of the Data from JavaScript code.
- Parameter data: The Data object to wrap
- Returns: An ArrayBuffer sharing memory with the Data object
*/
public static func wrap(dataWithoutCopy data: Data) -> ArrayBuffer {
let size = data.count
let unamanagedData = Unmanaged.passRetained(data as NSData)
let pointer: UnsafePointer<UInt8> = unamanagedData
.takeUnretainedValue()
.bytes
.assumingMemoryBound(to: UInt8.self)
let mutablePtr = UnsafeMutablePointer(mutating: pointer)
// This should manage the memory of the underlying Data manually
return NativeArrayBuffer(wrapping: mutablePtr, count: size, cleanup: { unamanagedData.release() })
}
// MARK: - Allocate
/**
Allocates a new native ArrayBuffer of the given size.
- Parameter size: The size of the buffer in bytes
- Parameter initializeToZero: If true, all bytes are set to 0, otherwise they are uninitialized
- Returns: A new NativeArrayBuffer with the allocated memory
*/
public static func allocate(size: Int, initializeToZero: Bool = false) -> NativeArrayBuffer {
let data = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
if initializeToZero {
data.initialize(repeating: 0, count: size)
}
return NativeArrayBuffer(wrapping: data, count: size, cleanup: { data.deallocate() })
}
// MARK: - Copy
/**
Copies the given raw pointer into a new native ArrayBuffer.
- Parameter other: The pointer to copy data from
- Parameter count: The number of bytes to copy
- Returns: A new NativeArrayBuffer containing a copy of the data
*/
public static func copy(
of other: UnsafeRawPointer,
count: Int
) -> NativeArrayBuffer {
let copy = UnsafeMutablePointer<UInt8>.allocate(capacity: count)
copy.initialize(from: other.assumingMemoryBound(to: UInt8.self), count: count)
return NativeArrayBuffer(wrapping: copy, count: count, cleanup: { copy.deallocate() })
}
public static func copy(of other: UnsafeRawBufferPointer) throws -> NativeArrayBuffer {
guard let baseAddress = other.baseAddress else {
throw MissingBaseAddressError()
}
return ArrayBuffer.copy(of: baseAddress, count: other.count)
}
/**
Copies the given Data into a new native ArrayBuffer.
- Parameter data: The Data object to copy
- Returns: A new NativeArrayBuffer containing a copy of the data
- Throws: Exception if unable to access the Data's bytes
*/
public static func copy(data: Data) throws -> NativeArrayBuffer {
// 1. Create new `ArrayBuffer` of same size
let size = data.count
let arrayBuffer = ArrayBuffer.allocate(size: size)
// 2. Copy all bytes from `Data` into our new `ArrayBuffer`
try data.withUnsafeBytes { rawPointer in
guard let baseAddress = rawPointer.baseAddress else {
throw MissingBaseAddressError()
}
memcpy(arrayBuffer.rawPointer, baseAddress, size)
}
return arrayBuffer
}
/**
Copies the given ArrayBuffer into a new native ArrayBuffer.
- Parameter other: The ArrayBuffer to copy
- Returns: A new NativeArrayBuffer containing a copy of the data
*/
public static func copy(of other: ArrayBuffer) -> NativeArrayBuffer {
ArrayBuffer.copy(of: other.rawPointer, count: other.byteLength)
}
}
// MARK: - Data
extension ArrayBuffer: ContiguousBytes {
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
try body(UnsafeRawBufferPointer(start: self.rawPointer, count: self.byteLength))
}
}
/**
An exception thrown when `baseAddress` of `UnsafeMutableRawBufferPointer` is `nil`.
*/
public final class MissingBaseAddressError: Exception, @unchecked Sendable {
override public var reason: String {
"Cannot get baseAddress of given data"
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Generic ArrayBuffer with an associated raw buffer type.
*/
public class GenericArrayBuffer<BufferType: RawArrayBuffer>: ArrayBuffer {
internal convenience init(_ backingBuffer: BufferType) {
self.init(backingBuffer as RawArrayBuffer)
}
}
/**
Native ArrayBuffer implementation that owns its memory and manages deallocation.
*/
public final class NativeArrayBuffer: GenericArrayBuffer<RawNativeArrayBuffer> {
convenience init(wrapping data: UnsafeMutableRawPointer, count: Int, cleanup: @escaping () -> Void) {
let backingBuffer = RawNativeArrayBuffer(data: data, size: count, cleanup: cleanup)
self.init(backingBuffer)
}
}
/**
JavaScript ArrayBuffer implementation that wraps a JavaScript ArrayBuffer object.
This provides a native Swift interface to ArrayBuffers created in JavaScript.
*/
public final class JavaScriptArrayBuffer: GenericArrayBuffer<RawJavaScriptArrayBuffer> {}

View File

@@ -0,0 +1,37 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A type-erased protocol that must be implemented by the definitions passed as ``ClassDefinition`` elements.
*/
public protocol AnyClassDefinitionElement: AnyDefinition {}
/**
Class definition element with an associated owner type. The `OwnerType` should refer to
the type that the parent `Class` definition is associated with (e.g. the shared object type).
*/
public protocol ClassDefinitionElement: AnyClassDefinitionElement {
associatedtype OwnerType
}
// MARK: - Conformance
// Allow some other definitions to be used as the class definition elements.
extension SyncFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension AsyncFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension ConcurrentFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension PropertyDefinition: ClassDefinitionElement {
// It already has the `OwnerType`
}
extension ConstantsDefinition: ClassDefinitionElement {
public typealias OwnerType = Void
}

View File

@@ -0,0 +1,152 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Represents a JavaScript class.
*/
public final class ClassDefinition: ObjectDefinition {
/**
Name of the class.
*/
let name: String
/**
A synchronous function that gets called when the object of this class is initializing.
*/
let constructor: AnySyncFunctionDefinition?
/**
A dynamic type for the associated object class.
*/
let associatedType: AnyDynamicType?
/**
Whether the associatedType inherits from `SharedRef`.
*/
let isSharedRef: Bool
init<AssociatedObject: ClassAssociatedObject>(
name: String,
associatedType: AssociatedObject.Type,
elements: [AnyClassDefinitionElement] = []
) {
self.name = name
self.constructor = elements.first(where: isConstructor) as? AnySyncFunctionDefinition
self.associatedType = ~AssociatedObject.self
self.isSharedRef = AssociatedObject.self is AnySharedRef.Type
// Constructors can't be passed down to the object definition
// as we shouldn't override the default `<Class>.prototype.constructor`.
let elementsWithoutConstructors = elements.filter({ !isConstructor($0) })
super.init(definitions: elementsWithoutConstructors)
}
// MARK: - JavaScriptObjectBuilder
@JavaScriptActor
public override func build(appContext: AppContext) throws -> JavaScriptObject {
let constructorBlock: ClassConstructorBlock = { [weak self, weak appContext] this, arguments in
guard let self, let appContext else {
let exception = NSException(
name: NSExceptionName("ExpoClassConstructorException"),
reason: "Call to function '\(String(describing: self?.name)).constructor' has been rejected.\n→ Caused by: App context was lost",
userInfo: nil
)
exception.raise()
return
}
// Call the native constructor when defined.
do {
if let constructor {
let result = try constructor.call(by: this, withArguments: arguments, appContext: appContext)
// Register the shared object if returned by the constructor.
if let result = result as? SharedObject {
appContext.sharedObjectRegistry.add(native: result, javaScript: this)
}
}
} catch let error as Exception {
let exception = NSException(
name: NSExceptionName("ExpoClassConstructorException"),
reason: error.description,
userInfo: ["code": error.code]
)
exception.raise()
} catch {
let exception = NSException(
name: NSExceptionName("ExpoClassConstructorException"),
reason: error.localizedDescription,
userInfo: nil
)
exception.raise()
}
}
let klass = try createClass(appContext: appContext, name: name, consturctor: constructorBlock)
try decorate(object: klass, appContext: appContext)
// Register the JS class and its associated native type.
if let sharedObjectType = associatedType as? DynamicSharedObjectType {
appContext.classRegistry.register(nativeClassId: sharedObjectType.typeIdentifier, javaScriptClass: klass)
}
return klass
}
@JavaScriptActor
public override func decorate(object: JavaScriptObject, appContext: AppContext) throws {
try decorateWithStaticFunctions(object: object, appContext: appContext)
// Here we actually don't decorate the input object (constructor) but its prototype.
// Properties are intentionally skipped here they have to decorate an instance instead of the prototype.
let prototype = object.getProperty("prototype").getObject()
try decorateWithConstants(object: prototype, appContext: appContext)
try decorateWithFunctions(object: prototype, appContext: appContext)
try decorateWithClasses(object: prototype, appContext: appContext)
try decorateWithProperties(object: prototype, appContext: appContext)
}
@JavaScriptActor
private func createClass(appContext: AppContext, name: String, consturctor: @escaping ClassConstructorBlock) throws -> JavaScriptObject {
if isSharedRef {
return try appContext.runtime.createSharedRefClass(name, constructor: consturctor)
}
return try appContext.runtime.createSharedObjectClass(name, constructor: consturctor)
}
}
// MARK: - ClassAssociatedObject
/**
A protocol for types that can be used an associated type of the ``ClassDefinition``.
*/
internal protocol ClassAssociatedObject {}
// Basically we only need these two
extension JavaScriptObject: ClassAssociatedObject, AnyArgument, AnyJavaScriptValue {
public static func convert(from value: JavaScriptValue, appContext: AppContext) throws -> Self {
guard value.kind == .object else {
throw Conversions.ConvertingException<JavaScriptObject>(value)
}
return value.getObject() as! Self
}
}
extension SharedObject: ClassAssociatedObject {}
// MARK: - Privates
/**
Checks whether the definition item is a constructor a synchronous function whose name is "constructor".
We do it that way for the following two reasons:
- It's easier to reuse existing `SyncFunctionDefinition`.
- Redefining prototype's `constructor` is a bad idea so a function with this name
needs to be filtered out when decorating the prototype.
*/
private func isConstructor(_ item: AnyDefinition) -> Bool {
return (item as? AnySyncFunctionDefinition)?.name == "constructor"
}

View File

@@ -0,0 +1,31 @@
// Copyright 2023-present 650 Industries. All rights reserved.
internal final class ClassRegistry {
var nativeToJS = [ObjectIdentifier: JavaScriptWeakObject]()
// MARK: - Accessing
func getJavaScriptClass(nativeClassId: ObjectIdentifier) -> JavaScriptObject? {
return nativeToJS[nativeClassId]?.lock()
}
func getJavaScriptClass(nativeClass: SharedObject.Type) -> JavaScriptObject? {
let nativeClassId = ObjectIdentifier(nativeClass)
return getJavaScriptClass(nativeClassId: nativeClassId)
}
// MARK: - Registration
func register(nativeClassId: ObjectIdentifier, javaScriptClass: JavaScriptObject) {
nativeToJS[nativeClassId] = javaScriptClass.createWeak()
}
func register(nativeClass: SharedObject.Type, javaScriptClass: JavaScriptObject) {
let nativeClassId = ObjectIdentifier(nativeClass)
register(nativeClassId: nativeClassId, javaScriptClass: javaScriptClass)
}
internal func clear() {
nativeToJS.removeAll()
}
}

View File

@@ -0,0 +1,336 @@
public struct Conversions {
/**
Converts an array to tuple. Because of tuples nature, it's not possible to convert an array of any size, so we can support only up to some fixed size.
*/
static func toTuple(_ array: [Any?]) throws -> Any? {
switch array.count {
case 0:
return ()
case 1:
return (array[0])
case 2:
return (array[0], array[1])
case 3:
return (array[0], array[1], array[2])
case 4:
return (array[0], array[1], array[2], array[3])
case 5:
return (array[0], array[1], array[2], array[3], array[4])
case 6:
return (array[0], array[1], array[2], array[3], array[4], array[5])
case 7:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6])
case 8:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7])
case 9:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7], array[8])
case 10:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7], array[8], array[9])
default:
throw TooManyArgumentsException((count: array.count, limit: 10))
}
}
static func fromNSObject(_ object: Any) -> Any {
switch object {
case let object as NSArray:
return object.map { Conversions.fromNSObject($0) }
case let object as NSDictionary:
let keyValuePairs: [(String, Any)] = object.map { ($0 as! String, Conversions.fromNSObject($1)) }
return Dictionary(uniqueKeysWithValues: keyValuePairs)
case is NSNull:
return Optional<Any>.none as Any
default:
return object
}
}
/**
Picks values under given keys from the dictionary, cast to a specific type. Can throw exceptions when
- The dictionary is missing some of the given keys (`MissingKeysException`)
- Some of the values cannot be cast to specified type (`CastingValuesException`)
*/
public static func pickValues<ValueType>(from dict: [String: Any], byKeys keys: [String], as type: ValueType.Type) throws -> [ValueType] {
var result = (
values: [ValueType](),
missingKeys: [String](),
invalidKeys: [String]()
)
for key in keys {
if dict[key] == nil {
result.missingKeys.append(key)
}
if let value = dict[key] as? ValueType {
result.values.append(value)
} else {
result.invalidKeys.append(key)
}
}
if !result.missingKeys.isEmpty {
throw MissingKeysException<ValueType>(result.missingKeys)
}
if !result.invalidKeys.isEmpty {
throw CastingValuesException<ValueType>(result.invalidKeys)
}
return result.values
}
/**
Converts color string to `UIColor` or throws an exception if the string is corrupted.
*/
static func toColor(colorString: String) throws -> UIColor {
let input = colorString.trimmingCharacters(in: .whitespacesAndNewlines)
// Handle RGB format
if input.hasPrefix("rgb") {
return try fromRGBString(input)
}
var hexStr = input
.replacingOccurrences(of: "#", with: "")
// If just RGB, set alpha to maximum
if hexStr.count == 6 { hexStr += "FF" }
if hexStr.count == 3 { hexStr += "F" }
// Expand short form (supported by Web)
if hexStr.count == 4 {
let chars = Array(hexStr)
hexStr = [
String(repeating: chars[0], count: 2),
String(repeating: chars[1], count: 2),
String(repeating: chars[2], count: 2),
String(repeating: chars[3], count: 2)
].joined(separator: "")
}
var rgba: UInt64 = 0
guard hexStr.range(of: #"^[0-9a-fA-F]{8}$"#, options: .regularExpression) != nil,
Scanner(string: hexStr).scanHexInt64(&rgba) else {
throw InvalidHexColorException(input)
}
return try toColor(rgba: rgba)
}
private static func fromRGBString(_ rgbString: String) throws -> UIColor {
let components = rgbString
.replacingOccurrences(of: "rgba(", with: "")
.replacingOccurrences(of: "rgb(", with: "")
.replacingOccurrences(of: ")", with: "")
.split(separator: ",")
.compactMap { Double($0.trimmingCharacters(in: .whitespaces)) }
guard components.count >= 3,
components[0] >= 0 && components[0] <= 255,
components[1] >= 0 && components[1] <= 255,
components[2] >= 0 && components[2] <= 255 else {
throw InvalidRGBColorException(rgbString)
}
let alpha = components.count > 3 ? Double(components[3]) : 1.0
return UIColor(
red: CGFloat(components[0]) / 255.0,
green: CGFloat(components[1]) / 255.0,
blue: CGFloat(components[2]) / 255.0,
alpha: alpha)
}
/**
Converts an integer for ARGB color to `UIColor`. Since the alpha channel is represented by first 8 bits,
it's optional out of the box. React Native converts colors to such format.
*/
static func toColor(argb: UInt64) throws -> UIColor {
guard argb <= UInt32.max else {
throw HexColorOverflowException(argb)
}
let alpha = CGFloat((argb >> 24) & 0xff) / 255.0
let red = CGFloat((argb >> 16) & 0xff) / 255.0
let green = CGFloat((argb >> 8) & 0xff) / 255.0
let blue = CGFloat(argb & 0xff) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
/**
Converts an integer for RGBA color to `UIColor`.
*/
static func toColor(rgba: UInt64) throws -> UIColor {
guard rgba <= UInt32.max else {
throw HexColorOverflowException(rgba)
}
let red = CGFloat((rgba >> 24) & 0xff) / 255.0
let green = CGFloat((rgba >> 16) & 0xff) / 255.0
let blue = CGFloat((rgba >> 8) & 0xff) / 255.0
let alpha = CGFloat(rgba & 0xff) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
/**
Formats an array of keys to the string with keys in apostrophes separated by commas.
*/
static func formatKeys(_ keys: [String]) -> String {
return keys.map { "`\($0)`" }.joined(separator: ", ")
}
static func formatPlural(_ number: Int, _ singular: String, _ plural: String? = nil) -> String {
return String(number) + (number == 1 ? singular : (plural ?? singular + "s"))
}
/**
Converts the function result to the type compatible with JavaScript.
*/
static func convertFunctionResult<ValueType>(
_ value: ValueType?,
appContext: AppContext? = nil,
dynamicType: AnyDynamicType? = nil
) -> Any {
if let appContext {
// Dynamic type is provided
if dynamicType as? DynamicVoidType == nil, let result = try? dynamicType?.convertResult(value as Any, appContext: appContext) {
return result
}
// Dynamic type can be obtained from the value
if let value = value as? AnyArgument, let result = try? type(of: value).getDynamicType().convertResult(value as Any, appContext: appContext) {
return result
}
}
return convertFunctionResultInRuntime(value, appContext: appContext)
}
/**
Converts the function result to the type that can later be converted to a JS value.
As opposed to `convertFunctionResult`, it has no information about the dynamic type,
so it is quite limited, e.g. it does not handle shared objects.
Currently it is required to handle results of the promise.
*/
static func convertFunctionResultInRuntime<ValueType>(_ value: ValueType?, appContext: AppContext? = nil) -> Any {
if let value = value as? Record {
return value.toDictionary(appContext: appContext)
}
if let value = value as? [Record] {
return value.map { $0.toDictionary(appContext: appContext) }
}
if let value = value as? any Enumerable {
return value.anyRawValue
}
return value as Any
}
// MARK: - Exceptions
/**
An exception meaning that the number of arguments exceeds the limit.
*/
internal class TooManyArgumentsException: GenericException<(count: Int, limit: Int)> {
override var reason: String {
"Native function expects \(formatPlural(param.limit, "argument")), but received \(param.count)"
}
}
/**
An exception thrown when the native value cannot be converted to JavaScript value.
*/
internal final class ConversionToJSFailedException: GenericException<(kind: JavaScriptValueKind, nativeType: Any.Type)> {
override var code: String {
"ERR_CONVERTING_TO_JS_FAILED"
}
override var reason: String {
"Conversion from native '\(param.nativeType)' to JavaScript value of type '\(param.kind.rawValue)' failed"
}
}
/**
An exception thrown when the JavaScript value cannot be converted to native value.
*/
internal final class ConversionToNativeFailedException: GenericException<(kind: JavaScriptValueKind, nativeType: Any.Type)> {
override var code: String {
"ERR_CONVERTING_TO_NATIVE_FAILED"
}
override var reason: String {
"Conversion from JavaScript value of type '\(param.kind.rawValue)' to native '\(param.nativeType)' failed"
}
}
/**
An exception that can be thrown by convertible types, when given value cannot be converted.
*/
public class ConvertingException<TargetType>: GenericException<Any?> {
public override var code: String {
"ERR_CONVERTING_FAILED"
}
public override var reason: String {
"Cannot convert '\(String(describing: param))' to \(TargetType.self)"
}
}
/**
An exception that is thrown when given value cannot be cast.
*/
internal class CastingException<TargetType>: GenericException<Any> {
override var code: String {
"ERR_CASTING_FAILED"
}
override var reason: String {
"Cannot cast '\(String(describing: param))' to \(TargetType.self)"
}
}
/**
An exception that can be thrown by convertible types,
when the values in given dictionary cannot be cast to specific type.
*/
internal class CastingValuesException<ValueType>: GenericException<[String]> {
override var code: String {
"ERR_CASTING_VALUES_FAILED"
}
override var reason: String {
"Cannot cast keys \(formatKeys(param)) to \(ValueType.self)"
}
}
/**
An exception that can be thrown by convertible types,
when given dictionary is missing some required keys.
*/
internal class MissingKeysException<ValueType>: GenericException<[String]> {
override var reason: String {
"Missing keys \(formatKeys(param)) to create \(ValueType.self) record"
}
}
/**
An exception that is thrown when null value is tried to be cast to non-optional type.
*/
internal class NullCastException<TargetType>: Exception {
override var reason: String {
"Cannot cast null to non-optional '\(TargetType.self)'"
}
}
/**
An exception used when the hex color string is invalid (e.g. contains non-hex characters).
*/
internal class InvalidHexColorException: GenericException<String> {
override var reason: String {
"Provided hex color '\(param)' is invalid"
}
}
/**
An exception used when the rgb color string is invalid.
*/
internal class InvalidRGBColorException: GenericException<String> {
override var reason: String {
"Provided rgb color string '\(param)' is invalid"
}
}
/**
An exception used when the integer value of the color would result in an overflow of `UInt32`.
*/
internal class HexColorOverflowException: GenericException<UInt64> {
override var reason: String {
"Provided hex color '\(param)' would result in an overflow"
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2024-present 650 Industries. All rights reserved.
import CoreMedia
extension CMTime: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CMTime {
if let seconds = value as? Double {
return CMTime(seconds: seconds, preferredTimescale: .max)
}
if let seconds = value as? any BinaryInteger {
return CMTime(seconds: Double(seconds), preferredTimescale: .max)
}
if let time = value as? CMTime {
return time
}
throw Conversions.ConvertingException<CMTime>(value)
}
}

View File

@@ -0,0 +1,307 @@
// Copyright 2022-present 650 Industries. All rights reserved.
extension UIColor: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
return try convert(from: value)
}
public static func convert(from value: Any?) throws -> Self {
// swiftlint:disable force_cast
if let value = value as? String {
if let namedColorComponents = namedColors[value] {
return uiColorWithComponents(namedColorComponents.map { $0 / 255 }) as! Self
}
return try Conversions.toColor(colorString: value) as! Self
}
if let components = value as? [Double] {
return uiColorWithComponents(components) as! Self
}
if let value = value as? Int {
return try Conversions.toColor(argb: UInt64(value)) as! Self
}
// Handle `PlatformColor` and `DynamicColorIOS`
if let opaqueValue = value as? [String: Any] {
if let semanticName = opaqueValue["semantic"] as? String,
let color = resolveNamedColor(name: semanticName) {
return color as! Self
}
if let semanticArray = opaqueValue["semantic"] as? [String] {
for semanticName in semanticArray {
if let color = resolveNamedColor(name: semanticName) {
return color as! Self
}
}
}
if let appearances = opaqueValue["dynamic"] as? [String: Any],
let lightColor = try appearances["light"].map({ try UIColor.convert(from: $0) }),
let darkColor = try appearances["dark"].map({ try UIColor.convert(from: $0) }) {
let highContrastLightColor = try appearances["highContrastLight"].map({ try UIColor.convert(from: $0) })
let highContrastDarkColor = try appearances["highContrastDark"].map({ try UIColor.convert(from: $0) })
#if os(iOS) || os(tvOS)
let color = UIColor { (traitCollection: UITraitCollection) -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
if traitCollection.accessibilityContrast == .high, let highContrastDarkColor {
return highContrastDarkColor
}
return darkColor
}
if traitCollection.accessibilityContrast == .high, let highContrastLightColor {
return highContrastLightColor
}
return lightColor
}
#elseif os(macOS)
let color = NSColor(name: nil) { (appearance: NSAppearance) -> NSColor in
let isDarkMode = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
let isHighContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
if isDarkMode {
if isHighContrast, let highContrastDarkColor = highContrastDarkColor {
return highContrastDarkColor
}
return darkColor
}
if isHighContrast, let highContrastLightColor = highContrastLightColor {
return highContrastLightColor
}
return lightColor
}
#endif
return color as! Self
}
}
if let color = value as? Self {
return color
}
throw Conversions.ConvertingException<UIColor>(value)
// swiftlint:enable force_cast
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
if let value = result as? UIColor {
return value.string
}
return result
}
}
extension UIColor {
public var string: String {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
#if os(macOS)
getRed(&r, green: &g, blue: &b, alpha: &a)
return String(format: "#%02x%02x%02x%02x", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255) )
#else
if getRed(&r, green: &g, blue: &b, alpha: &a) {
return String(format: "#%02x%02x%02x%02x", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255) )
}
#endif
return "#00000000"
}
}
extension CGColor: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
// swiftlint:disable force_cast
do {
return try UIColor.convert(from: value, appContext: appContext).cgColor as! Self
} catch _ as Conversions.ConvertingException<UIColor> {
// Rethrow `ConvertingError` with proper type
throw Conversions.ConvertingException<CGColor>(value)
}
// swiftlint:enable force_cast
}
public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
let cgColor = result as! CGColor
let uiColor = UIColor(cgColor: cgColor)
return try UIColor.convertResult(uiColor, appContext: appContext)
}
}
private func resolveNamedColor(name: String) -> UIColor? {
return UIColor(named: name) ?? uiColorFromSemanticName(name: name)
}
private func uiColorFromSemanticName(name: String) -> UIColor? {
let selector: Selector
if name.hasSuffix("Color") {
selector = Selector(name)
} else {
selector = Selector("\(name)Color")
}
guard UIColor.responds(to: selector) else {
return nil
}
return UIColor.perform(selector).takeUnretainedValue() as? UIColor
}
private func uiColorWithComponents(_ components: [Double]) -> UIColor {
let alpha = components.count > 3 ? components[3] : 1.0
return UIColor(red: components[0], green: components[1], blue: components[2], alpha: alpha)
}
/**
Color components for named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color)
and additionally the transparent color.
*/
private let namedColors: [String: [Double]] = [
"aliceblue": [240, 248, 255, 255],
"antiquewhite": [250, 235, 215, 255],
"aqua": [0, 255, 255, 255],
"aquamarine": [127, 255, 212, 255],
"azure": [240, 255, 255, 255],
"beige": [245, 245, 220, 255],
"bisque": [255, 228, 196, 255],
"black": [0, 0, 0, 255],
"blanchedalmond": [255, 235, 205, 255],
"blue": [0, 0, 255, 255],
"blueviolet": [138, 43, 226, 255],
"brown": [165, 42, 42, 255],
"burlywood": [222, 184, 135, 255],
"cadetblue": [95, 158, 160, 255],
"chartreuse": [127, 255, 0, 255],
"chocolate": [210, 105, 30, 255],
"coral": [255, 127, 80, 255],
"cornflowerblue": [100, 149, 237, 255],
"cornsilk": [255, 248, 220, 255],
"crimson": [220, 20, 60, 255],
"cyan": [0, 255, 255, 255],
"darkblue": [0, 0, 139, 255],
"darkcyan": [0, 139, 139, 255],
"darkgoldenrod": [184, 134, 11, 255],
"darkgray": [169, 169, 169, 255],
"darkgreen": [0, 100, 0, 255],
"darkgrey": [169, 169, 169, 255],
"darkkhaki": [189, 183, 107, 255],
"darkmagenta": [139, 0, 139, 255],
"darkolivegreen": [85, 107, 47, 255],
"darkorange": [255, 140, 0, 255],
"darkorchid": [153, 50, 204, 255],
"darkred": [139, 0, 0, 255],
"darksalmon": [233, 150, 122, 255],
"darkseagreen": [143, 188, 143, 255],
"darkslateblue": [72, 61, 139, 255],
"darkslategray": [47, 79, 79, 255],
"darkslategrey": [47, 79, 79, 255],
"darkturquoise": [0, 206, 209, 255],
"darkviolet": [148, 0, 211, 255],
"deeppink": [255, 20, 147, 255],
"deepskyblue": [0, 191, 255, 255],
"dimgray": [105, 105, 105, 255],
"dimgrey": [105, 105, 105, 255],
"dodgerblue": [30, 144, 255, 255],
"firebrick": [178, 34, 34, 255],
"floralwhite": [255, 250, 240, 255],
"forestgreen": [34, 139, 34, 255],
"fuchsia": [255, 0, 255, 255],
"gainsboro": [220, 220, 220, 255],
"ghostwhite": [248, 248, 255, 255],
"gold": [255, 215, 0, 255],
"goldenrod": [218, 165, 32, 255],
"gray": [128, 128, 128, 255],
"green": [0, 128, 0, 255],
"greenyellow": [173, 255, 47, 255],
"grey": [128, 128, 128, 255],
"honeydew": [240, 255, 240, 255],
"hotpink": [255, 105, 180, 255],
"indianred": [205, 92, 92, 255],
"indigo": [75, 0, 130, 255],
"ivory": [255, 255, 240, 255],
"khaki": [240, 230, 140, 255],
"lavender": [230, 230, 250, 255],
"lavenderblush": [255, 240, 245, 255],
"lawngreen": [124, 252, 0, 255],
"lemonchiffon": [255, 250, 205, 255],
"lightblue": [173, 216, 230, 255],
"lightcoral": [240, 128, 128, 255],
"lightcyan": [224, 255, 255, 255],
"lightgoldenrodyellow": [250, 250, 210, 255],
"lightgray": [211, 211, 211, 255],
"lightgreen": [144, 238, 144, 255],
"lightgrey": [211, 211, 211, 255],
"lightpink": [255, 182, 193, 255],
"lightsalmon": [255, 160, 122, 255],
"lightseagreen": [32, 178, 170, 255],
"lightskyblue": [135, 206, 250, 255],
"lightslategray": [119, 136, 153, 255],
"lightslategrey": [119, 136, 153, 255],
"lightsteelblue": [176, 196, 222, 255],
"lightyellow": [255, 255, 224, 255],
"lime": [0, 255, 0, 255],
"limegreen": [50, 205, 50, 255],
"linen": [250, 240, 230, 255],
"magenta": [255, 0, 255, 255],
"maroon": [128, 0, 0, 255],
"mediumaquamarine": [102, 205, 170, 255],
"mediumblue": [0, 0, 205, 255],
"mediumorchid": [186, 85, 211, 255],
"mediumpurple": [147, 112, 219, 255],
"mediumseagreen": [60, 179, 113, 255],
"mediumslateblue": [123, 104, 238, 255],
"mediumspringgreen": [0, 250, 154, 255],
"mediumturquoise": [72, 209, 204, 255],
"mediumvioletred": [199, 21, 133, 255],
"midnightblue": [25, 25, 112, 255],
"mintcream": [245, 255, 250, 255],
"mistyrose": [255, 228, 225, 255],
"moccasin": [255, 228, 181, 255],
"navajowhite": [255, 222, 173, 255],
"navy": [0, 0, 128, 255],
"oldlace": [253, 245, 230, 255],
"olive": [128, 128, 0, 255],
"olivedrab": [107, 142, 35, 255],
"orange": [255, 165, 0, 255],
"orangered": [255, 69, 0, 255],
"orchid": [218, 112, 214, 255],
"palegoldenrod": [238, 232, 170, 255],
"palegreen": [152, 251, 152, 255],
"paleturquoise": [175, 238, 238, 255],
"palevioletred": [219, 112, 147, 255],
"papayawhip": [255, 239, 213, 255],
"peachpuff": [255, 218, 185, 255],
"peru": [205, 133, 63, 255],
"pink": [255, 192, 203, 255],
"plum": [221, 160, 221, 255],
"powderblue": [176, 224, 230, 255],
"purple": [128, 0, 128, 255],
"rebeccapurple": [102, 51, 153, 255],
"red": [255, 0, 0, 255],
"rosybrown": [188, 143, 143, 255],
"royalblue": [65, 105, 225, 255],
"saddlebrown": [139, 69, 19, 255],
"salmon": [250, 128, 114, 255],
"sandybrown": [244, 164, 96, 255],
"seagreen": [46, 139, 87, 255],
"seashell": [255, 245, 238, 255],
"sienna": [160, 82, 45, 255],
"silver": [192, 192, 192, 255],
"skyblue": [135, 206, 235, 255],
"slateblue": [106, 90, 205, 255],
"slategray": [112, 128, 144, 255],
"slategrey": [112, 128, 144, 255],
"snow": [255, 250, 250, 255],
"springgreen": [0, 255, 127, 255],
"steelblue": [70, 130, 180, 255],
"tan": [210, 180, 140, 255],
"teal": [0, 128, 128, 255],
"thistle": [216, 191, 216, 255],
"tomato": [255, 99, 71, 255],
"transparent": [0, 0, 0, 0],
"turquoise": [64, 224, 208, 255],
"violet": [238, 130, 238, 255],
"wheat": [245, 222, 179, 255],
"white": [255, 255, 255, 255],
"whitesmoke": [245, 245, 245, 255],
"yellow": [255, 255, 0, 255],
"yellowgreen": [154, 205, 50, 255]
]

View File

@@ -0,0 +1,79 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A protocol whose intention is to wrap any type
to keep its real signature and not type-erase it by the compiler.
*/
public protocol AnyDynamicType: CustomStringConvertible, Sendable {
/**
Checks whether the inner type is the same as the given type.
*/
func wraps<InnerType>(_ type: InnerType.Type) -> Bool
/**
Checks whether the dynamic type is equal to another,
that is when the type of the dynamic types are equal and their inner types are equal.
*/
func equals(_ type: AnyDynamicType) -> Bool
/**
Preliminarily casts the given JavaScriptValue to a non-JS value that the other `cast` function can handle.
It **must** be run on the thread used by the JavaScript runtime.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any
/**
Casts the given value to the wrapped type and returns it as `Any`.
NOTE: It may not be just simple type-casting (e.g. when the wrapped type conforms to `Convertible`).
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue
/**
Converts function's result to the type that can later be converted to a JS value.
For instance, types such as records, enumerables and shared objects need special handling
and conversion to simpler types (dictionary, primitive value or specific JS value).
*/
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any
}
extension AnyDynamicType {
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
return jsValue.getRaw() as Any
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return value
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
// This conversion isn't the most efficient way to convert Objective-C value to JS value.
// Better performance should be provided in dynamic type specializations.
return try JavaScriptValue.from(value, runtime: appContext.runtime)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
return result
}
}
// MARK: - Operators
infix operator ~>
public func ~> <T>(lhs: AnyDynamicType, rhs: T.Type) -> Bool {
return lhs.wraps(rhs)
}
infix operator !~>
public func !~> <T>(lhs: AnyDynamicType, rhs: T.Type) -> Bool {
return !lhs.wraps(rhs)
}
public func == (lhs: AnyDynamicType, rhs: AnyDynamicType) -> Bool {
return lhs.equals(rhs)
}
public func != (lhs: AnyDynamicType, rhs: AnyDynamicType) -> Bool {
return !lhs.equals(rhs)
}

View File

@@ -0,0 +1,57 @@
// Copyright 2015-present 650 Industries. All rights reserved.
internal struct DynamicArrayBufferType: AnyDynamicType {
let innerType: AnyArrayBuffer.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let arrayBufferType = type as? Self {
return arrayBufferType.innerType == innerType
}
return false
}
/**
Converts JS array buffer to its native representation.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let rawArrayBuffer = jsValue.getArrayBuffer() else {
throw NotArrayBufferException(innerType)
}
let jsArrayBuffer = JavaScriptArrayBuffer(rawArrayBuffer)
return switch innerType {
case is JavaScriptArrayBuffer.Type: jsArrayBuffer
case is NativeArrayBuffer.Type: jsArrayBuffer.copy()
// this might happen when a user implemented own subclass of ArrayBuffer
// or uses 'ArrayBuffer' directly
default: throw ArrayBufferArgumentTypeException(innerType)
}
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
guard let arrayBuffer = result as? ArrayBuffer else {
throw Conversions.ConversionToJSFailedException((kind: .object, nativeType: ResultType.self))
}
return arrayBuffer.backingBuffer
}
var description: String {
return String(describing: Data.self)
}
}
internal final class NotArrayBufferException: GenericException<AnyArrayBuffer.Type>, @unchecked Sendable {
override var reason: String {
"Given argument is not an instance of \(param)"
}
}
internal final class ArrayBufferArgumentTypeException: GenericException<AnyArrayBuffer.Type>, @unchecked Sendable {
override var reason: String {
"\(param) cannot be used as argument type. Use either JavaScriptArrayBuffer or NativeArrayBuffer"
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type representing array types. Requires the array's element type
for the initialization as it delegates casting to that type for each element in the array.
*/
internal struct DynamicArrayType: AnyDynamicType {
let elementType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let ArrayType = InnerType.self as? AnyArray.Type {
return elementType.equals(ArrayType.getElementDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let arrayType = type as? Self {
return arrayType.elementType.equals(elementType)
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
let value = jsValue.isArray() ? jsValue.getArray() : [jsValue]
return try value.map { try elementType.cast(jsValue: $0, appContext: appContext) }
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? [Any] {
return try value.map { try elementType.cast($0, appContext: appContext) }
}
// We should probably throw an error if we get here. On the other side, the array type
// requirement can be more loosen so we can try to arrayize values that are not arrays.
return [try elementType.cast(value, appContext: appContext)]
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
if let result = result as? [Any] {
return try result.map({ element in
return try elementType.convertResult(element, appContext: appContext)
})
}
return result
}
var description: String {
"[\(elementType.description)]"
}
}
/**
A type-erased protocol used to recognize if the generic type is an array type.
`Array` is a generic type, so it's impossible to check the inheritance directly.
*/
internal protocol AnyArray {
/**
Exposes the `Element` generic type wrapped by the dynamic type to preserve its metadata.
*/
static func getElementDynamicType() -> AnyDynamicType
}
/**
Extends the `Array` type to expose its generic `Element` as a dynamic type.
*/
extension Array: AnyArray {
static func getElementDynamicType() -> AnyDynamicType {
return ~Element.self
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal struct DynamicBoolType: AnyDynamicType {
static let shared = DynamicBoolType()
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type == Swift.Bool.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if Optional.isNil(value) {
throw Conversions.NullCastException<Bool>()
}
// `as? Bool` matches any NSNumber with value 0 or 1, not just actual booleans
// This causes `Either<Bool, Double>` to incorrectly decode numbers 0/1 as Bool instead of Double.
if let nsNumber = value as? NSNumber {
guard CFGetTypeID(nsNumber) == CFBooleanGetTypeID() else {
throw Conversions.CastingException<Bool>(value)
}
return nsNumber.boolValue
}
throw Conversions.CastingException<Bool>(value)
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.kind == .bool {
return jsValue.getBool()
}
throw Conversions.ConversionToNativeFailedException((kind: jsValue.kind, nativeType: Bool.self))
}
var description: String {
"Bool"
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that wraps any type conforming to `Convertible` protocol.
*/
internal struct DynamicConvertibleType: AnyDynamicType {
let innerType: Convertible.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let convertibleType = type as? Self {
return convertibleType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return try innerType.convert(from: value, appContext: appContext)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
return try innerType.convertResult(result, appContext: appContext)
}
var description: String {
String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
A dynamic type representing Swift `Data` or Objective-C `NSData` type and backing by JavaScript `Uint8Array`.
*/
internal struct DynamicDataType: AnyDynamicType {
static let shared = DynamicDataType()
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return InnerType.self == Data.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Data.Type
}
/**
Converts JS typed array to its native representation.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let jsTypedArray = jsValue.getTypedArray(), jsTypedArray.kind == TypedArrayKind.Uint8Array else {
throw Conversions.CastingException<Uint8Array>(jsValue)
}
return Data(bytes: jsTypedArray.getUnsafeMutableRawPointer(), count: jsTypedArray.getProperty("byteLength").getInt())
}
var description: String {
return String(describing: Data.self)
}
}

View File

@@ -0,0 +1,67 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
A dynamic type representing dictionary types. Requires the dictionary's value type
for the initialization as it delegates casting to that type for each element in the dictionary.
*/
internal struct DynamicDictionaryType: AnyDynamicType {
let valueType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let dictionaryType = InnerType.self as? AnyDictionary.Type {
return valueType.equals(dictionaryType.getValueDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let dictionaryType = type as? Self {
return dictionaryType.valueType.equals(valueType)
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
let converter = appContext.converter
if let jsObject = try? jsValue.asObject() {
var result: [AnyHashable: Any] = [:]
for key in jsObject.getPropertyNames() {
result[key] = try converter.toNative(jsObject.getProperty(key), valueType)
}
return result
}
throw Conversions.CastingException<JavaScriptObject>(jsValue)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? [AnyHashable: Any] {
return try value.mapValues { try valueType.cast($0, appContext: appContext) }
}
throw Conversions.CastingException<[AnyHashable: Any]>(value)
}
var description: String {
"[Hashable: \(valueType.description)]"
}
}
/**
A type-erased protocol used to recognize if the generic type is a dictionary type.
`Dictionary` is a generic type, so it's impossible to check the inheritance directly.
*/
internal protocol AnyDictionary {
/**
Exposes the `Value` generic type wrapped by the dynamic type to preserve its metadata.
*/
static func getValueDynamicType() -> AnyDynamicType
}
/**
Extends the `Dictionary` type to expose its generic `Value` as a dynamic type.
*/
extension Dictionary: AnyDictionary {
static func getValueDynamicType() -> AnyDynamicType {
return ~Value.self
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal struct DynamicEitherType<EitherType: AnyEither>: AnyDynamicType {
let eitherType: EitherType.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return eitherType.dynamicTypes().contains { eitherType in
return eitherType.wraps(type)
}
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
let types = eitherType.dynamicTypes()
for type in types {
if let preliminaryValue = try? type.cast(jsValue: jsValue, appContext: appContext),
let value = try? type.cast(preliminaryValue, appContext: appContext) {
return EitherType(value)
}
}
throw NeitherTypeException(types)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? EitherType {
return value
}
let types = eitherType.dynamicTypes()
for type in types {
// Initialize the "either" when the current type can cast given value.
if let value = try? type.cast(value, appContext: appContext) {
return EitherType(value)
}
}
throw NeitherTypeException(types)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
guard let either = result as? EitherType else {
throw Conversions.CastingException<EitherType>(result)
}
let types = eitherType.dynamicTypes()
// Try each type - one should succeed
for type in types {
if let converted = try? type.convertResult(either.value, appContext: appContext) {
return converted
}
}
throw NeitherTypeException(types)
}
var description: String {
let types = eitherType.dynamicTypes()
return "Either<\(types.map(\.description).joined(separator: ", "))>"
}
}

View File

@@ -0,0 +1,47 @@
// Copyright 2025-present 650 Industries. All rights reserved.
/**
Dynamic type for values conforming to `Encodable` protocol.
Note that currently it can only encode from native to JavaScript values, thus cannot be used for arguments.
*/
internal struct DynamicEncodableType: AnyDynamicType {
static let shared = DynamicEncodableType()
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type is Encodable
}
func equals(_ type: any AnyDynamicType) -> Bool {
// Just mocking it here as we don't really need this function and we rather want to keep it a singleton
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
// TODO: Create DynamicDecodableType and reuse it here that would work perfectly with Codable types
fatalError("DynamicEncodableType can only cast to JavaScript, not from")
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
if let value = value as? JavaScriptValue {
return value
}
if let value = value as? Encodable {
let runtime = try appContext.runtime
let encoder = JSValueEncoder(runtime: runtime)
try value.encode(to: encoder)
return encoder.value
}
throw Conversions.ConversionToJSFailedException((kind: .object, nativeType: ValueType.self))
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
// TODO: We should get rid of this function, but it seems it's still used in some places
return try castToJS(result, appContext: appContext)
}
var description: String {
"Encodable"
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type representing an enum that conforms to `Enumerable`.
*/
internal struct DynamicEnumType: AnyDynamicType {
let innerType: any Enumerable.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let enumType = type as? Self {
return enumType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
// TODO: Remove if. The result should not be an enumerable, but it's converted in `MainValueConverter:21` to an enum.
if let value = value as? any Enumerable {
return value
}
return try innerType.create(fromRawValue: value)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
if let result = result as? any Enumerable {
return result.anyRawValue
}
return result
}
var description: String {
"Enum<\(innerType)>"
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A dynamic type representing various types of JavaScript values.
*/
internal struct DynamicJavaScriptType: AnyDynamicType {
let innerType: AnyJavaScriptValue.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let providedType = type as? Self {
return providedType.innerType == innerType
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
return try innerType.convert(from: jsValue, appContext: appContext)
}
var description: String {
return String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,72 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal struct DynamicNumberType<NumberType>: AnyDynamicType {
let numberType: NumberType.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type == numberType
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.kind == .number {
if let FloatingPointType = NumberType.self as? any BinaryFloatingPoint.Type {
return FloatingPointType.init(jsValue.getDouble())
}
if let IntegerType = NumberType.self as? any BinaryInteger.Type {
// JS stores all numbers as doubles, so using `getDouble` makes more sense
// than `getInt` and lets us do schoolbook rounding instead of floor.
return IntegerType.init(jsValue.getDouble().rounded())
}
}
throw Conversions.ConversionToNativeFailedException((kind: jsValue.kind, nativeType: numberType))
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let number = value as? NumberType {
return number
}
if let number = value as? Double, NumberType.self is Float32.Type {
// double -> float
return Float32.init(number)
}
if let number = value as? any BinaryInteger {
if let IntegerType = NumberType.self as? any BinaryInteger.Type {
// integer -> another integer
return IntegerType.init(number)
}
if let FloatingPointType = NumberType.self as? any BinaryFloatingPoint.Type {
// integer -> float
return FloatingPointType.init(number)
}
}
if let number = value as? any BinaryFloatingPoint {
if let FloatingPointType = NumberType.self as? any BinaryFloatingPoint.Type {
// float -> another float
return FloatingPointType.init(number)
}
if let IntegerType = NumberType.self as? any BinaryInteger.Type {
// float -> integer (schoolbook rounding)
return IntegerType.init(number.rounded())
}
}
throw Conversions.CastingException<NumberType>(value)
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
if let value = value as? any BinaryFloatingPoint {
return .number(Double(value))
}
if let value = value as? any BinaryInteger {
return .number(Double(value))
}
throw Conversions.ConversionToJSFailedException((kind: .number, nativeType: ValueType.self))
}
var description: String {
"\(numberType)"
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that represents an optional type, which allows `nil` to be passed when casting.
Requires the optional's wrapped type as it delegates casting to that type for non-nil values.
*/
internal struct DynamicOptionalType: AnyDynamicType {
let wrappedType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let OptionalType = InnerType.self as? AnyOptional.Type {
return wrappedType.equals(OptionalType.getWrappedDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let optionalType = type as? Self {
return optionalType.wrappedType.equals(wrappedType)
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.isUndefined() || jsValue.isNull() {
return Optional<Any>.none as Any
}
return try wrappedType.cast(jsValue: jsValue, appContext: appContext)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if Optional.isNil(value) || value is NSNull {
return Optional<Any>.none as Any
}
return try wrappedType.cast(value, appContext: appContext)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
// Delegate the conversion to the wrapped type
return try wrappedType.convertResult(result, appContext: appContext)
}
var description: String {
"\(wrappedType)?"
}
}
/**
A type-erased protocol used to recognize if the generic type is an optional type.
`Optional` is a generic enum, so it's impossible to check the inheritance directly.
*/
internal protocol AnyOptional {
/**
Exposes the `Wrapped` generic type wrapped by the dynamic type to preserve its metadata.`
*/
static func getWrappedDynamicType() -> AnyDynamicType
}
/**
Make generic `Optional` implement non-generic `AnyOptional` and add handy check against type-erased `nil`.
*/
extension Optional: AnyOptional {
static func getWrappedDynamicType() -> AnyDynamicType {
return ~Wrapped.self
}
static func isNil(_ object: Wrapped) -> Bool {
switch object as Any {
case Optional<Any>.none:
return true
default:
return false
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that can wrap any type, but it casts only type-compatible values using `as?` keyword.
The innermost type of the other dynamic types like `ArrayArgumentType` and `OptionalArgumentType`.
*/
internal struct DynamicRawType<InnerType>: AnyDynamicType {
let innerType: InnerType.Type
func wraps<AnyInnerType>(_ type: AnyInnerType.Type) -> Bool {
return type == innerType
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? InnerType {
return value
}
// Sometimes conversion from Double to Float will fail due to precision losses. We can accept them though.
if let value = value as? Double, wraps(Float.self) {
return Float(value)
}
// Raw types are always non-optional, but they may receive `nil` values.
// Let's throw more specific error in this case.
if Optional.isNil(value) {
throw Conversions.NullCastException<InnerType>()
}
throw Conversions.CastingException<InnerType>(value)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
// TODO: Definitions and JS object builders should have its own dynamic type.
// We use `DynamicRawType` for this only temporarily.
if let objectBuilder = result as? JavaScriptObjectBuilder {
return try JavaScriptActor.assumeIsolated {
return try objectBuilder.build(appContext: appContext)
} as Any
}
return result
}
var description: String {
String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2025-present 650 Industries. All rights reserved.
internal struct DynamicSerializableType: AnyDynamicType {
let innerType: any AnySerializable.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let serializableType = type as? Self {
return serializableType.innerType == innerType
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let runtime = appContext._runtime else {
throw Exceptions.RuntimeLost()
}
guard let jsSerializable = SerializableExtractor.extractSerializable(jsValue, runtime: runtime) else {
throw NotSerializableException(innerType)
}
return Serializable(jsSerializable)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let serializable = value as? Serializable else {
throw Conversions.ConvertingException<Serializable>(value)
}
return serializable
}
var description: String {
return String(describing: innerType)
}
}
internal final class NotSerializableException: GenericException<AnySerializable.Type>, @unchecked Sendable {
override var reason: String {
"Given argument is not an instance of \(param)"
}
}

View File

@@ -0,0 +1,114 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A dynamic type representing the `SharedObject` type and its subclasses.
*/
internal struct DynamicSharedObjectType: AnyDynamicType {
let innerType: AnySharedObject.Type
/**
A unique identifier of the wrapped type.
*/
var typeIdentifier: ObjectIdentifier {
return ObjectIdentifier(innerType)
}
init(innerType: AnySharedObject.Type) {
self.innerType = innerType
}
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let sharedObjectType = type as? Self {
return sharedObjectType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? SharedObject {
// Given value is a shared object already
return value
}
// If the given value is a shared object id, search the registry for its native representation
if let sharedObjectId = value as? SharedObjectId,
let nativeSharedObject = appContext.sharedObjectRegistry.get(sharedObjectId)?.native {
return nativeSharedObject
}
throw NativeSharedObjectNotFoundException()
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.kind == .number {
let sharedObjectId = jsValue.getInt() as SharedObjectId
guard let nativeSharedObject = appContext.sharedObjectRegistry.get(sharedObjectId)?.native else {
throw NativeSharedObjectNotFoundException()
}
return nativeSharedObject
}
if let jsObject = try? jsValue.asObject(),
let nativeSharedObject = appContext.sharedObjectRegistry.toNativeObject(jsObject) {
return nativeSharedObject
}
throw NativeSharedObjectNotFoundException()
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
// Postpone object creation to execute on the JS thread.
JavaScriptObjectBinding.init {
// If the result is a native shared object, create its JS representation and add the pair to the registry of shared objects.
if let sharedObject = result as? SharedObject {
// If the JS object already exists, just return it.
if let jsObject = sharedObject.getJavaScriptObject() {
return jsObject
}
guard let jsObject = (try? appContext.newObject(nativeClassId: typeIdentifier)) ?? (try? newBaseSharedObject(appContext, nativeType: innerType)) else {
// Throwing is not possible here due to swift-objC interop.
log.warn("Unable to create a JS object for \(description)")
return JavaScriptObject()
}
// Add newly created objects to the registry.
appContext.sharedObjectRegistry.add(native: sharedObject, javaScript: jsObject)
return jsObject
}
return JavaScriptObject()
}
}
var description: String {
return "SharedObject<\(innerType)>"
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
if let value = value as? JavaScriptObjectBinding {
return try JavaScriptValue.from(value.get(), runtime: appContext.runtime)
}
throw NativeSharedObjectNotFoundException()
}
}
private func getBaseSharedType(_ appContext: AppContext, nativeType: AnySharedObject.Type) throws -> JavaScriptObject {
let isSharedRef = nativeType is AnySharedRef.Type
return try JavaScriptActor.assumeIsolated {
return try isSharedRef ? appContext.runtime.getSharedRefClass() : appContext.runtime.getSharedObjectClass()
}
}
private func newBaseSharedObject(_ appContext: AppContext, nativeType: AnySharedObject.Type) throws -> JavaScriptObject? {
let jsClass = try getBaseSharedType(appContext, nativeType: nativeType)
let prototype = try jsClass.getProperty("prototype").asObject()
return try appContext.runtime.createObject(withPrototype: prototype)
}
internal final class NativeSharedObjectNotFoundException: Exception {
override var reason: String {
"Unable to find the native shared object associated with given JavaScript object"
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal struct DynamicStringType: AnyDynamicType {
static let shared = DynamicStringType()
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type == String.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let string = value as? String {
return string
}
throw Conversions.CastingException<String>(value)
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.kind == .string {
return jsValue.getString()
}
throw Conversions.ConversionToNativeFailedException((kind: jsValue.kind, nativeType: String.self))
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
if let string = value as? String {
return .string(string, runtime: try appContext.runtime)
}
throw Conversions.ConversionToJSFailedException((kind: .string, nativeType: ValueType.self))
}
var description: String {
"String"
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2025-present 650 Industries. All rights reserved.
internal struct DynamicSwiftUIViewType<ViewType: ExpoSwiftUIView>: AnyDynamicType {
let innerType: ViewType.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let viewType = type as? Self {
return viewType.innerType == innerType
}
return false
}
/**
Casts from the React component instance to the view tag (`Int`).
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let viewTag = findViewTag(jsValue) else {
throw InvalidViewTagException()
}
return viewTag
}
/**
Converts a value of type `Int` to a native view with that tag in the given app context.
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let viewTag = value as? Int else {
throw InvalidViewTagException()
}
guard Thread.isMainThread else {
throw NonMainThreadException()
}
if let view = appContext.findView(withTag: viewTag, ofType: ExpoSwiftUI.SwiftUIVirtualView<ViewType.Props, ViewType>.self) {
return view.contentView
}
guard let view = appContext.findView(withTag: viewTag, ofType: AnyExpoSwiftUIHostingView.self) else {
throw Exceptions.SwiftUIViewNotFound((tag: viewTag, type: innerType.self))
}
return view.getContentView()
}
var description: String {
return "View<\(innerType)>"
}
}
private func findViewTag(_ value: JavaScriptValue) -> Int? {
if value.isNumber() {
return value.getInt()
}
if value.isObject() {
let nativeTag = value.getObject().getProperty("nativeTag")
if nativeTag.isNumber() {
return nativeTag.getInt()
}
}
return nil
}

View File

@@ -0,0 +1,44 @@
// Copyright 2021-present 650 Industries. All rights reserved.
// Function names should start with a lowercase character, but in this one case
// we want it to be uppercase as we treat it more like a generic class.
// swiftlint:disable identifier_name
/**
Factory creating an instance of the dynamic type wrapper conforming to `AnyDynamicType`.
Depending on the given type, it may return one of `DynamicArrayType`, `DynamicOptionalType`, `DynamicConvertibleType`, etc.
It does some type checks in runtime when the type's conformance/inheritance is unknown for the compiler.
See the `~` prefix operator overloads that are used for types known for the compiler.
You can add more type checks for types that don't conform to `AnyArgument`, but are allowed to be used as return types.
`Void` is a good example as it cannot conform to anything or language protocols that cannot be extended to implement `AnyArgument`.
*/
private func DynamicType<T>(_ type: T.Type) -> AnyDynamicType {
if let AnyArgumentType = T.self as? AnyArgument.Type {
return AnyArgumentType.getDynamicType()
}
if T.self == Void.self {
return DynamicVoidType.shared
}
if T.self is Encodable.Type {
// There is no dedicated `~` operator overload for encodables to avoid ambiguity
// when the type is both `AnyArgument` and `Encodable` (e.g. strings, numeric types).
return DynamicEncodableType.shared
}
return DynamicRawType(innerType: T.self)
}
/**
Handy prefix operator that makes the dynamic type from the static type.
*/
prefix operator ~
internal prefix func ~ <T>(type: T.Type) -> AnyDynamicType {
return DynamicType(type)
}
internal prefix func ~ <T>(type: T.Type) -> AnyDynamicType where T: AnyArgument {
return T.getDynamicType()
}
internal prefix func ~ (type: Void.Type) -> AnyDynamicType {
return DynamicVoidType.shared
}

View File

@@ -0,0 +1,57 @@
// Copyright 2022-present 650 Industries. All rights reserved.
internal struct DynamicTypedArrayType: AnyDynamicType {
let innerType: AnyTypedArray.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let typedArrayType = type as? Self {
return typedArrayType.innerType == innerType
}
return false
}
/**
Converts JS typed array to its native representation.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let jsTypedArray = jsValue.getTypedArray() else {
throw NotTypedArrayException(innerType)
}
return TypedArray.create(from: jsTypedArray)
}
/**
Converts the given native `TypedArray` to a concrete typed array class wrapped by the dynamic type.
Throws `ArrayTypeMismatchException` otherwise.
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let typedArray = value as? TypedArray else {
throw NotTypedArrayException(innerType)
}
// Concrete typed arrays must be the same as the inner type.
guard innerType == TypedArray.self || type(of: typedArray) == innerType else {
throw ArrayTypeMismatchException((received: type(of: typedArray), expected: innerType))
}
return typedArray
}
var description: String {
return String(describing: innerType)
}
}
internal final class ArrayTypeMismatchException: GenericException<(received: Any.Type, expected: Any.Type)> {
override var reason: String {
"Received a typed array of type \(param.received), expected \(param.expected)"
}
}
internal final class NotTypedArrayException: GenericException<AnyTypedArray.Type> {
override var reason: String {
"Given argument is not an instance of \(param)"
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2021-present 650 Industries. All rights reserved.
internal struct DynamicValueOrUndefinedType<InnerType: AnyArgument>: AnyDynamicType {
let innerType: InnerType.Type = InnerType.self
let dynamicInnerType: AnyDynamicType = InnerType.getDynamicType()
func wraps<AnyInnerType>(_ type: AnyInnerType.Type) -> Bool {
return innerType == type
}
func equals(_ type: any AnyDynamicType) -> Bool {
return type is Self
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.isUndefined() {
return ValueOrUndefined<InnerType>.undefined
}
return try dynamicInnerType.cast(jsValue: jsValue, appContext: appContext)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if value is ValueOrUndefined<InnerType> {
return value
}
return ValueOrUndefined<InnerType>.value(unwrapped: try dynamicInnerType.cast(value, appContext: appContext) as! InnerType)
}
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
let value = result as! ValueOrUndefined<InnerType>
if case .undefined = value {
// JavaScriptValue.undefined is not runtime specific, so it's safe to return here, even if it's not on the JS thread.
return JavaScriptValue.undefined
}
return try dynamicInnerType.convertResult(value.optional, appContext: appContext)
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
if let jaValue = value as? JavaScriptValue {
return jaValue
}
return try dynamicInnerType.castToJS(value, appContext: appContext)
}
var description: String {
return "ValueOrUndefined<\(dynamicInnerType)>"
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2023-present 650 Industries. All rights reserved.
internal struct DynamicViewType: AnyDynamicType {
let innerType: UIView.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let viewType = type as? Self {
return viewType.innerType == innerType
}
return false
}
/**
Casts from the React component instance to the view tag (`Int`).
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let viewTag = findViewTag(jsValue) else {
throw InvalidViewTagException()
}
return viewTag
}
/**
Converts a value of type `Int` to a native view with that tag in the given app context.
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let viewTag = value as? Int else {
throw InvalidViewTagException()
}
guard Thread.isMainThread else {
throw NonMainThreadException()
}
guard let view = appContext.findView(withTag: viewTag, ofType: innerType.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: innerType.self))
}
return view
}
var description: String {
return "View<\(innerType)>"
}
}
private func findViewTag(_ value: JavaScriptValue) -> Int? {
if value.isNumber() {
return value.getInt()
}
if value.isObject() {
let nativeTag = value.getObject().getProperty("nativeTag")
if nativeTag.isNumber() {
return nativeTag.getInt()
}
}
return nil
}
internal final class InvalidViewTagException: Exception {
override var reason: String {
"The view tag must be a number"
}
}
internal final class NonMainThreadException: Exception {
override var reason: String {
"All operations on the views must run from the main UI thread"
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal struct DynamicVoidType: AnyDynamicType {
static let shared = DynamicVoidType()
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type == Void.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
return Optional<Any>.none as Any
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return value
}
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
return .undefined
}
var description: String {
"Void"
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2025-present 650 Industries. All rights reserved.
internal struct DynamicWorkletType: AnyDynamicType {
let serializableDynamicType = DynamicSerializableType(innerType: Worklet.self)
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return Worklet.self == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let serializable = try serializableDynamicType.cast(jsValue: jsValue, appContext: appContext) as? Serializable else {
throw NotSerializableException(Worklet.self)
}
return try Worklet(serializable)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let worklet = value as? Worklet else {
throw Conversions.ConvertingException<Worklet>(value)
}
return worklet
}
var description: String {
return String(describing: Worklet.self)
}
}

View File

@@ -0,0 +1,63 @@
/**
Represents a listener for the specific event.
*/
internal struct EventListener: AnyDefinition {
let name: EventName
let call: (Any?, Any?) throws -> Void
/**
Listener initializer for events without sender and payload.
*/
init(_ name: EventName, _ listener: @escaping () -> Void) {
self.name = name
self.call = { _, _ in listener() }
}
/**
Listener initializer for events with no payload.
*/
init<Sender>(_ name: EventName, _ listener: @escaping (Sender) -> Void) {
self.name = name
self.call = { sender, _ in
guard let sender = sender as? Sender else {
throw InvalidSenderTypeException((eventName: name, senderType: Sender.self))
}
listener(sender)
}
}
/**
Listener initializer for events that specify the payload.
*/
init<Sender, PayloadType>(_ name: EventName, _ listener: @escaping (Sender, PayloadType?) -> Void) {
self.name = name
self.call = { sender, payload in
guard let sender = sender as? Sender else {
throw InvalidSenderTypeException((eventName: name, senderType: Sender.self))
}
listener(sender, payload as? PayloadType)
}
}
}
final class InvalidSenderTypeException: GenericException<(eventName: EventName, senderType: Any.Type)>, @unchecked Sendable {
override var reason: String {
"Sender for event '\(param.eventName)' must be of type \(param.senderType)"
}
}
public enum EventName: Equatable, Sendable {
case custom(_ name: String)
// MARK: Module lifecycle
case moduleCreate
case moduleDestroy
case appContextDestroys
// MARK: App (UIApplication) lifecycle
case appEntersForeground
case appBecomesActive
case appEntersBackground
}

View File

@@ -0,0 +1,66 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
An alias for type-erased callback handler.
*/
typealias AnyCallbackHandlerType = @convention(block) ([String: Any]) -> Void
/**
Public type-erased protocol that `Callback` object conforms to.
*/
public protocol AnyCallback {
/**
Initializes an empty callback (no-op).
*/
init()
}
/**
Internal type-erased protocol for `Callback` object.
*/
internal protocol AnyCallbackInternal: AnyCallback {
/**
Sets the callback handler. By default the callback
is not settled which means it has no handler, thus is no-op.
*/
func settle(_ handler: @escaping AnyCallbackHandlerType)
/**
Invalidates the callback, making its handler no-op.
*/
func invalidate()
}
/**
Callable object that represents a JavaScript function.
*/
public class Callback<ArgType>: AnyCallback, AnyCallbackInternal {
/**
The underlying closure to invoke when the callback is called.
*/
private var handler: AnyCallbackHandlerType?
// MARK: AnyCallback
public required init() {}
// MARK: AnyCallbackInternal
internal func settle(_ handler: @escaping AnyCallbackHandlerType) {
self.handler = handler
}
internal func invalidate() {
self.handler = nil
}
// MARK: Calling as function
/**
Allows the callback instance to be called as a function.
*/
public func callAsFunction(_ arg: [String: Any]) {
// TODO: Convert records to dictionaries (@tsapeta)
handler?(arg)
}
}

View File

@@ -0,0 +1,90 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
An object that can dispatch native events.
*/
public final class EventDispatcher {
/**
Type of the event dispatcher handler.
- Note: It must be marked as `@convention(block)` as long as we support Objective-C views and thus need to use `setValue(_:forKey:)`.
*/
internal typealias Handler = @convention(block) ([String: Any]) -> Void
/**
A custom name of the dispatching event. The default name is usually a name of the property holding the dispatcher.
*/
internal var customName: String?
/**
A function that is invoked to dispatch the event, no-op by default.
Something that manages the events should override it.
*/
internal var handler: Handler?
public var onEventSent: (([String: Any]) -> Void)?
/**
Default initializer of the event dispatcher. Provide a custom name if you want the dispatcher
to refer to an event with different name than the property holding the dispatcher.
*/
public init(_ customName: String? = nil) {
self.customName = customName
}
// MARK: - Calling as function
/**
Dispatches the event with the given dictionary as a payload.
*/
public func callAsFunction(_ payload: [String: Any]) {
handler?(payload)
onEventSent?(payload)
}
/**
Dispatches the event with the given record as a payload.
*/
public func callAsFunction(_ payload: Record) {
handler?(payload.toDictionary())
onEventSent?(payload.toDictionary())
}
/**
Dispatches the event with an empty payload.
*/
public func callAsFunction() {
handler?([:])
onEventSent?([:])
}
}
/**
Installs convenient event dispatchers for the given event and view.
The provided handler can be specific to Paper or Fabric.
*/
internal func installEventDispatcher<ViewType>(forEvent eventName: String, onView view: ViewType, handler: @escaping EventDispatcher.Handler) {
// Find view's property that is of type `EventDispatcher` and refers to this particular event name.
let child = Mirror(reflecting: view).children.first {
return isEventDispatcherWithName($0, eventName)
}
if let eventDispatcher = child?.value as? EventDispatcher {
eventDispatcher.handler = handler
} else if let view = view as? UIView, view.responds(to: Selector(eventName)) {
// This is to handle events in legacy views written in Objective-C.
// Note that the property should be of type EXDirectEventBlock.
view.setValue(handler, forKey: eventName)
} else {
log.warn("Couldn't find the property for event '\(eventName)' in '\(type(of: view))' class")
}
}
/**
Checks whether the mirror child refers to the event dispatcher with the given event name.
*/
internal func isEventDispatcherWithName(_ mirrorChild: Mirror.Child, _ eventName: String) -> Bool {
guard let eventDispatcher = mirrorChild.value as? EventDispatcher else {
return false
}
return eventDispatcher.customName != nil ? eventDispatcher.customName == eventName : mirrorChild.label == eventName
}

View File

@@ -0,0 +1,80 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal enum EventObservingType: String {
case startObserving
case stopObserving
}
internal protocol AnyEventObservingDefinition: AnyDefinition, Sendable {
var event: String? { get }
var type: EventObservingType { get }
func call()
}
public final class EventObservingDefinition: AnyEventObservingDefinition, @unchecked Sendable {
public typealias ClosureType = () -> Void
let type: EventObservingType
let event: String?
let closure: ClosureType
init(type: EventObservingType, event: String?, _ closure: @escaping ClosureType) {
self.type = type
self.event = event
self.closure = closure
}
func call() {
closure()
}
}
public struct EventObservingDecorator: JavaScriptObjectDecorator {
let definitions: [any AnyEventObservingDefinition]
/**
Decorates the given object with `startObserving` and `stopObserving` functions.
These functions are automatically called by the `EventEmitter` implementation.
*/
@JavaScriptActor
func decorate(object: JavaScriptObject, appContext: AppContext) throws {
// We need to keep track the number of observed events
// so we can call observers not attached to any event in the right moment.
var observingEvents: Int = 0
let startObserving = AsyncFunctionDefinition(
EventObservingType.startObserving.rawValue,
firstArgType: String.self,
dynamicArgumentTypes: [~String.self]
) { (event: String) in
observingEvents += 1
for definition in definitions where definition.type == .startObserving {
if definition.event == event || (observingEvents == 1 && definition.event == nil) {
definition.call()
}
}
}
let stopObserving = AsyncFunctionDefinition(
EventObservingType.stopObserving.rawValue,
firstArgType: String.self,
dynamicArgumentTypes: [~String.self]
) { (event: String) in
observingEvents -= 1
for definition in definitions where definition.type == .stopObserving {
if definition.event == event || (observingEvents == 0 && definition.event == nil) {
definition.call()
}
}
}
object.setProperty(startObserving.name, value: try startObserving.build(appContext: appContext))
object.setProperty(stopObserving.name, value: try stopObserving.build(appContext: appContext))
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2024-present 650 Industries. All rights reserved.
@available(*, deprecated, message: "Use `sendEvent` directly on the module instance instead")
public final class LegacyEventEmitterCompat {
internal weak var appContext: AppContext?
init(appContext: AppContext) {
self.appContext = appContext
}
// Objective-C protocol doesn't specify nullability
// swiftlint:disable:next implicitly_unwrapped_optional
public func sendEvent(withName name: String!, body: Any!) {
guard let appContext, let runtime = try? appContext.runtime else {
log.warn("Unable to send an event '\(String(describing: name))' because the runtime is not available")
return
}
// `Any` is not sendable, so we must trick the compiler.
let eventArguments = NonisolatedUnsafeVar<[Any]>([body as Any])
// Send the event to all modules that declare support for this particular event.
// That's how it works in the device event emitter provided by React Native.
let moduleHoldersWithEvent = appContext.moduleRegistry.filter { holder in
return holder.definition.eventNames.contains(name)
}
runtime.schedule {
for holder in moduleHoldersWithEvent {
if let jsObject = holder.javaScriptObject {
JSUtils.emitEvent(name, to: jsObject, withArguments: eventArguments.value, in: runtime)
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
An exception that may have been caused by another error.
*/
public protocol ChainableException: Error, AnyObject {
/**
The direct cause of the exception.
*/
var cause: Error? { get set }
/**
The first error that started the chain of exceptions.
*/
var rootCause: Error? { get }
/**
Sets the direct cause of the exception and returns itself.
*/
func causedBy(_ error: Error?) -> Self
/**
Tells whether any of the cause in chain is of given type.
*/
func isCausedBy<ErrorType: Error>(_ errorType: ErrorType.Type) -> Bool
}
public extension ChainableException {
var rootCause: Error? {
if let cause = cause as? ChainableException {
return cause.rootCause ?? cause
}
return cause
}
@discardableResult
func causedBy(_ error: Error?) -> Self {
cause = error ?? cause
return self
}
func isCausedBy<ErrorType: Error>(_ errorType: ErrorType.Type) -> Bool {
if cause is ErrorType {
return true
}
if let cause = cause as? ChainableException {
return cause.isCausedBy(errorType)
}
return false
}
}

View File

@@ -0,0 +1,55 @@
import Foundation
/**
A protocol for errors specyfing its `code` and providing the `description`.
*/
public protocol CodedError: Error, CustomStringConvertible {
var code: String { get }
var description: String { get }
}
/**
Extends the `CodedError` to make a fallback for `code` and `description`.
*/
public extension CodedError {
/**
The code is inferred from the class name e.g. the code of `ModuleNotFoundError` becomes `ERR_MODULE_NOT_FOUND`.
To obtain the code, the class name is cut off from generics and `Error` suffix, then it's converted to snake case and uppercased.
*/
var code: String {
return errorCodeFromString(String(describing: type(of: self)))
}
/**
The description falls back to object's localized description.
*/
var description: String {
return localizedDescription
}
}
/**
Basic implementation of `CodedError` protocol,
where the code and the description are provided in the initializer.
*/
public struct SimpleCodedError: CodedError {
public var code: String
public var description: String
init(_ code: String, _ description: String) {
self.code = code
self.description = description
}
}
func errorCodeFromString(_ str: String) -> String {
let name = str.replacingOccurrences(of: #"(Error|Exception)?(<.*>)?$"#, with: "", options: .regularExpression)
// The pattern is valid, so it'll never throw
// swiftlint:disable:next force_try
let regex = try! NSRegularExpression(pattern: "(.)([A-Z])", options: [])
let range = NSRange(location: 0, length: name.count)
return "ERR_" + regex
.stringByReplacingMatches(in: name, options: [], range: range, withTemplate: "$1_$2")
.uppercased()
}

View File

@@ -0,0 +1,76 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A group of the most common exceptions that might be necessary for modules.
*/
public struct Exceptions {
/**
The Expo app context is no longer available.
*/
public final class AppContextLost: Exception {
override public var reason: String {
"The app context has been lost"
}
}
/**
The JavaScript runtime is no longer available.
*/
public final class RuntimeLost: Exception {
override public var reason: String {
"The JavaScript runtime has been lost"
}
}
/**
The UI runtime is no longer available.
*/
public final class UIRuntimeLost: Exception {
override public var reason: String {
"The UI runtime has been lost"
}
}
/**
An exception to throw when the operation is not supported on the simulator.
*/
public final class SimulatorNotSupported: Exception {
override public var reason: String {
"This operation is not supported on the simulator"
}
}
/**
An exception to throw when the view with the given tag and class cannot be found.
*/
public final class ViewNotFound<ViewType: UIView>: GenericException<(tag: Int, type: ViewType.Type)> {
override public var reason: String {
"Unable to find the '\(param.type)' view with tag '\(param.tag)'"
}
}
public final class SwiftUIViewNotFound<ViewType: ExpoSwiftUIView>: GenericException<(tag: Int, type: ViewType.Type)> {
override public var reason: String {
"Unable to find the '\(param.type)' view with tag '\(param.tag)'"
}
}
/**
An exception to throw when there is no module implementing the `EXFileSystemInterface` interface.
*/
public final class FileSystemModuleNotFound: Exception {
override public var reason: String {
"FileSystem module not found, make sure 'expo-file-system' is linked correctly"
}
}
/**
An exception to throw when there is no module implementing the `EXPermissionsInterface` interface.
- Note: This should never happen since the module is a part of `expo-modules-core`, but for compatibility reasons
`appContext.permissions` is still an optional value.
*/
public final class PermissionsModuleNotFound: Exception {
override public var reason: String {
"Permissions module not found"
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2022-present 650 Industries. All rights reserved.
open class Exception: CodedError, ChainableException, CustomStringConvertible, CustomDebugStringConvertible, @unchecked Sendable {
open lazy var name: String = String(describing: Self.self)
/**
String describing the reason of the exception.
*/
open var reason: String {
"undefined reason"
}
/**
The origin in code where the exception was created.
*/
open var origin: ExceptionOrigin
/**
A custom code of the exception. When unset, the `code` is calculated from the exception name or class name.
*/
let customCode: String?
/**
The default initializer that captures the place in the code where the exception was created.
- Warning: Call it only without arguments!
*/
public init(file: String = #fileID, line: UInt = #line, function: String = #function) {
self.origin = ExceptionOrigin(file: file, line: line, function: function)
self.customCode = nil
}
public init(name: String, description: String, code: String? = nil, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.origin = ExceptionOrigin(file: file, line: line, function: function)
self.customCode = code
self.name = name
self.description = description
}
// MARK: - CodedError
open var code: String {
customCode ?? errorCodeFromString(name)
}
// MARK: - ChainableException
open var cause: Error?
// MARK: - CustomStringConvertible
open lazy var description: String = concatDescription(reason, withCause: cause, debug: false)
// MARK: - CustomDebugStringConvertible
open var debugDescription: String {
let debugDescription = "\(name): \(reason) (at \(origin.file):\(origin.line))"
return concatDescription(debugDescription, withCause: cause, debug: true)
}
}
/**
Concatenates the exception description with its cause description.
*/
private func concatDescription(_ description: String, withCause cause: Error?, debug: Bool = false) -> String {
let causeSeparator = "\n→ Caused by: "
switch cause {
case let cause as Exception:
return description + causeSeparator + (debug ? cause.debugDescription : cause.description)
case let cause as CodedError:
// `CodedError` is deprecated but we need to provide backwards compatibility as some modules already used it.
return description + causeSeparator + cause.description
case let cause?:
return description + causeSeparator + cause.localizedDescription
default:
return description
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Represents the place in code where the exception was created.
*/
public struct ExceptionOrigin: CustomStringConvertible, Sendable {
/**
The path to the file in which the exception was created.
*/
let file: String
/**
The line number on which the exception was created.
*/
let line: UInt
/**
The name (selector) of the declaration in which the exception was created.
*/
let function: String
/**
Stringified representation of the exception origin.
*/
public var description: String {
"at \(file):\(line) in \(function)"
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
The exception that needs some additional parameters to be best described.
*/
open class GenericException<ParamType>: Exception, @unchecked Sendable {
/**
The additional parameter passed to the initializer.
*/
public let param: ParamType
/**
The default initializer that takes a param and captures the place in the code where the exception was created.
- Warning: Call it only with one argument! If you need to pass more parameters, use a tuple instead.
*/
public init(_ param: ParamType, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.param = param
super.init(file: file, line: line, function: function)
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Exception wrapper used to handle unexpected internal native errors.
*/
public class UnexpectedException: Exception {
private let errorDescription: String
public init(_ error: Error, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.errorDescription = error.localizedDescription
super.init(file: file, line: line, function: function)
}
public override var reason: String {
return errorDescription
}
}

View File

@@ -0,0 +1,19 @@
// Copyright 2024-present 650 Industries. All rights reserved.
@protocol RCTBridgeModule;
#import <ExpoModulesCore/EXNativeModulesProxy.h>
#import <ExpoModulesCore/EXModuleRegistry.h>
@class EXAppContext;
@interface ExpoBridgeModule : NSObject <RCTBridgeModule>
@property(nonatomic, nullable, strong) EXAppContext *appContext;
- (nonnull instancetype)initWithAppContext:(nonnull EXAppContext *)appContext;
- (void)legacyProxyDidSetBridge:(nonnull EXNativeModulesProxy *)moduleProxy
legacyModuleRegistry:(nonnull EXModuleRegistry *)moduleRegistry;
@end

View File

@@ -0,0 +1,84 @@
// Copyright 2024-present 650 Industries. All rights reserved.
#import <ReactCommon/RCTTurboModule.h>
#import <ExpoModulesCore/ExpoBridgeModule.h>
#import <ExpoModulesCore/EXRuntime.h>
#import <ExpoModulesCore/Swift.h>
@implementation ExpoBridgeModule
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE(ExpoModulesCore);
- (instancetype)init
{
if (self = [super init]) {
_appContext = [[EXAppContext alloc] init];
}
return self;
}
- (instancetype)initWithAppContext:(EXAppContext *) appContext
{
if (self = [super init]) {
_appContext = appContext;
}
return self;
}
+ (BOOL)requiresMainQueueSetup
{
// We do want to run the initialization (`setBridge`) on the JS thread.
return NO;
}
- (void)setBridge:(RCTBridge *)bridge
{
_bridge = bridge;
[self maybeSetupAppContext];
}
- (void)maybeSetupAppContext
{
if (!_bridge) {
return;
}
EXRuntime *runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:_bridge];
// If `global.expo` is defined, the app context has already been initialized from `ExpoReactNativeFactory`.
// The factory was introduced in SDK 55 and requires migration in bare workflow projects.
// We keep this as an alternative way during the transitional period.
if (runtime && ![[runtime global] hasProperty:@"expo"]) {
NSLog(@"Expo is being initialized from the deprecated ExpoBridgeModule, make sure to migrate to ExpoReactNativeFactory in your project");
_appContext.reactBridge = _bridge;
_appContext._runtime = runtime;
[_appContext registerNativeModules];
}
}
/**
This should be called inside `[EXNativeModulesProxy setBridge:]`.
*/
- (void)legacyProxyDidSetBridge:(nonnull EXNativeModulesProxy *)moduleProxy
legacyModuleRegistry:(nonnull EXModuleRegistry *)moduleRegistry
{
_appContext.legacyModulesProxy = moduleProxy;
_appContext.legacyModuleRegistry = moduleRegistry;
}
/**
A synchronous method that is called from JS before requiring
any module to ensure that all necessary bindings are installed.
*/
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(installModules)
{
if (_bridge && !_appContext._runtime) {
// If `setBridge:` was called but the runtime was not found, we try again here.
[self maybeSetupAppContext];
}
return nil;
}
@end

View File

@@ -0,0 +1,27 @@
// Copyright 2025-present 650 Industries. All rights reserved.
/**
Class that extends the standard JavaScript runtime with some Expo-specific features.
For instance, the global `expo` object is available only in Expo runtimes.
*/
public extension ExpoRuntime {
@JavaScriptActor
internal func initializeCoreObject(_ coreObject: JavaScriptObject) throws {
global().defineProperty(EXGlobalCoreObjectPropertyName, value: coreObject, options: .enumerable)
}
@JavaScriptActor
internal func getCoreObject() throws -> JavaScriptObject {
return try global().getProperty(EXGlobalCoreObjectPropertyName).asObject()
}
@JavaScriptActor
internal func getSharedObjectClass() throws -> JavaScriptObject {
return try getCoreObject().getProperty("SharedObject").asObject()
}
@JavaScriptActor
internal func getSharedRefClass() throws -> JavaScriptObject {
return try getCoreObject().getProperty("SharedRef").asObject()
}
}

View File

@@ -0,0 +1,100 @@
/**
An alias to `Result<Any, Exception>` which can be passed to the function callback.
*/
public typealias FunctionCallResult = Result<Any, Exception>
/**
A protocol for any type-erased function.
*/
internal protocol AnyFunctionDefinition: AnyDefinition, JavaScriptObjectBuilder {
/**
Name of the function. JavaScript refers to the function by this name.
*/
var name: String { get }
/**
An array of the dynamic types that the function takes. If the last type is `Promise`, it's not included.
*/
var dynamicArgumentTypes: [AnyDynamicType] { get }
/**
A number of arguments the function takes. If the function expects to receive an owner (`this`) as the first argument, it's not counted.
Similarly, if the last argument is of type `Promise`, it is not counted.
*/
var argumentsCount: Int { get }
/**
A minimum number of arguments the functions needs which equals to `argumentsCount` reduced by the number of trailing optional arguments.
*/
var requiredArgumentsCount: Int { get }
/**
Indicates whether the function's arguments starts from the owner that calls this function.
*/
var takesOwner: Bool { get set }
/**
Calls the function with a given owner and arguments and returns a result through the callback block.
- Parameters:
- owner: An object that calls this function. If the `takesOwner` property is true
and type of the first argument matches the owner type, it's being passed as the argument.
- args: An array of arguments to pass to the function. They could be Swift primitives
when invoked through the bridge and in unit tests or `JavaScriptValue`s
when the function is called through the JSI.
- appContext: An app context where the function is being executed.
- callback: A callback that receives a result of the function execution.
*/
func call(
by owner: AnyObject?,
withArguments args: [Any],
appContext: AppContext,
callback: @Sendable @escaping (FunctionCallResult) -> Void
)
}
extension AnyFunctionDefinition {
var requiredArgumentsCount: Int {
var trailingOptionalArgumentsCount: Int = 0
for dynamicArgumentType in dynamicArgumentTypes.reversed() {
if dynamicArgumentType is DynamicOptionalType {
trailingOptionalArgumentsCount += 1
} else {
break
}
}
return argumentsCount - trailingOptionalArgumentsCount
}
var argumentsCount: Int {
return dynamicArgumentTypes.count
}
/**
Calls the function just like `call(by:withArguments:appContext:callback:)`, but without an owner
and with an empty callback. Might be useful when you only want to call the function,
but don't care about the result.
*/
func call(withArguments args: [Any], appContext: AppContext) {
call(by: nil, withArguments: args, appContext: appContext, callback: { _ in })
}
}
internal final class FunctionCallException: GenericException<String>, @unchecked Sendable {
override var reason: String {
"Calling the '\(param)' function has failed"
}
override var code: String {
guard let cause = cause as? Exception else {
return super.code
}
return cause.code
}
}
internal final class ArgumentConversionException: Exception, @unchecked Sendable {
override var reason: String {
"Failed to downcast arguments"
}
}

View File

@@ -0,0 +1,204 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import Dispatch
/**
Type-erased protocol for asynchronous functions.
*/
internal protocol AnyAsyncFunctionDefinition: AnyFunctionDefinition {
/**
Specifies on which queue the function should run.
*/
@discardableResult
func runOnQueue(_ queue: DispatchQueue?) -> Self
}
/**
The default queue used for module's async function calls.
*/
private let defaultQueue = DispatchQueue(label: "expo.modules.AsyncFunctionQueue", qos: .userInitiated)
/**
Represents a function that can only be called asynchronously, thus its JavaScript equivalent returns a Promise.
*/
public class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFunctionDefinition, @unchecked Sendable {
typealias ClosureType = (Args) throws -> ReturnType
/**
The underlying closure to run when the function is called.
*/
let body: ClosureType
/**
Bool value indicating whether the function takes promise as the last argument.
*/
let takesPromise: Bool
/**
Dispatch queue on which each function's call is run.
*/
var queue: DispatchQueue?
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
_ body: @escaping ClosureType
) {
self.name = name
self.takesPromise = dynamicArgumentTypes.last?.wraps(Promise.self) ?? false
self.dynamicArgumentTypes = dynamicArgumentTypes
self.body = body
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0) - (takesPromise ? 1 : 0)
}
var takesOwner: Bool = false
func call(
by owner: AnyObject?,
withArguments args: [Any],
appContext: AppContext,
callback: @Sendable @escaping (FunctionCallResult) -> Void
) {
let promise = Promise(appContext: appContext) { value in
callback(.success(Conversions.convertFunctionResult(value, appContext: appContext, dynamicType: ~ReturnType.self)))
} rejecter: { exception in
callback(.failure(exception))
}
var arguments: [Any] = concat(
arguments: args,
withOwner: owner,
withPromise: takesPromise ? promise : nil,
forFunction: self,
appContext: appContext
)
do {
try validateArgumentsNumber(function: self, received: args.count)
// All `JavaScriptValue` args must be preliminarly converted on the JS thread, so before we jump to the function's queue.
arguments = try cast(jsValues: arguments, forFunction: self, appContext: appContext)
} catch let error as Exception {
callback(.failure(error))
return
} catch {
callback(.failure(UnexpectedException(error)))
return
}
let queue = queue ?? defaultQueue
dispatchOnQueueUntilViewRegisters(appContext: appContext, arguments: arguments, queue: queue) { [body, name] in
let returnedValue: ReturnType?
do {
// Convert arguments to the types desired by the function.
arguments = try cast(arguments: arguments, forFunction: self, appContext: appContext)
// swiftlint:disable:next force_cast
let argumentsTuple = try Conversions.toTuple(arguments) as! Args
returnedValue = try body(argumentsTuple)
} catch let error as Exception {
promise.reject(FunctionCallException(name).causedBy(error))
return
} catch {
promise.reject(UnexpectedException(error))
return
}
if !self.takesPromise {
promise.resolve(returnedValue)
}
}
}
/**
* Checks if the `AsyncFunction` is a method of a `View`, if it is and the `View` has not yet been registered in the view registry it
* re-dispatches the block until the view registers. The block can be re-dispatched up to three times before the cast is considered failed.
* This is a sub-optimal solution, but the only one until we get access to the runtime scheduler. In the vast majority of cases the block
* will be dispatched without any retries,
*/
private func dispatchOnQueueUntilViewRegisters(
appContext: AppContext,
arguments: [Any],
queue: DispatchQueue,
retryCount: Int = 0,
_ block: @escaping () -> Void
) {
// Empirically a single retry is enough, use three just to be safe
let maxRetryCount = 3
queue.async {
// Checks if this is a view function unregistered in the view registry. The check can be performed from the main thread only.
if retryCount < maxRetryCount,
let viewTag = arguments.first as? Int,
let uiManager = appContext.reactBridge?.uiManager,
self.dynamicArgumentTypes.first is DynamicViewType,
Thread.isMainThread, // swiftlint:disable:next legacy_objc_type
uiManager.view(forReactTag: NSNumber(value: viewTag)) == nil {
// Schedule the block on the original queue through UI manager if view is missing in the registry.
self.dispatchOnQueueUntilViewRegisters(appContext: appContext, arguments: arguments, queue: queue, retryCount: retryCount + 1, block)
return
}
// Schedule the block as normal.
block()
}
}
// MARK: - JavaScriptObjectBuilder
@JavaScriptActor
func build(appContext: AppContext) throws -> JavaScriptObject {
// It seems to be safe to capture a strong reference to `self` here. This is needed for detached functions, that are not part of the module definition.
// Module definitions are held in memory anyway, but detached definitions (returned by other functions) are not, so we need to capture them here.
// It will be deallocated when that JS host function is garbage-collected by the JS VM.
return try appContext.runtime.createAsyncFunction(name, argsCount: argumentsCount) { [self] this, args, resolve, reject in
self.call(by: this, withArguments: args, appContext: appContext) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, nil)
case .success(let value):
resolve(value)
}
}
}
}
// MARK: - AnyAsyncFunctionDefinition
public func runOnQueue(_ queue: DispatchQueue?) -> Self {
self.queue = queue
return self
}
}
extension AsyncFunctionDefinition {
var requiredArgumentsCount: Int {
var trailingOptionalArgumentsCount: Int = 0
let reversedArgumentTypes = dynamicArgumentTypes.reversed()
let reversedArgumentsToIterate: any Sequence<AnyDynamicType> = takesPromise
? reversedArgumentTypes.dropFirst()
: reversedArgumentTypes
for dynamicArgumentType in reversedArgumentsToIterate {
if dynamicArgumentType is DynamicOptionalType {
trailingOptionalArgumentsCount += 1
} else {
break
}
}
return argumentsCount - trailingOptionalArgumentsCount
}
}

View File

@@ -0,0 +1,128 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Type-erased protocol for asynchronous functions using Swift concurrency
*/
internal protocol AnyConcurrentFunctionDefinition: AnyFunctionDefinition {
/**
Specifies if the main actor should be used. Necessary when attached to a view
*/
var requiresMainActor: Bool { get set }
}
/**
Represents a concurrent function that can only be called asynchronously, thus its JavaScript equivalent returns a Promise.
As opposed to `AsyncFunctionDefinition`, it can leverage the new Swift's concurrency model and take the async/await closure.
*/
public final class ConcurrentFunctionDefinition<Args, FirstArgType, ReturnType>: AnyConcurrentFunctionDefinition, @unchecked Sendable {
typealias ClosureType = (Args) async throws -> ReturnType
let body: ClosureType
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
_ body: @escaping ClosureType
) {
self.name = name
self.body = body
self.dynamicArgumentTypes = dynamicArgumentTypes
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0)
}
var takesOwner: Bool = false
var requiresMainActor: Bool = false
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @Sendable @escaping (FunctionCallResult) -> Void) {
// We have to trick the compiler here to make the arguments sendable and nonisolated, otherwise they couldn't be captured.
// TODO: Find a way to structure this code better, that is more in line with the concurrency model.
let arguments = NonisolatedUnsafeVar<[Any]>([])
do {
try validateArgumentsNumber(function: self, received: args.count)
arguments.value = concat(
arguments: args,
withOwner: owner,
withPromise: nil,
forFunction: self,
appContext: appContext
)
// All `JavaScriptValue` args must be preliminarly converted on the JS thread, before we jump to the function's queue.
arguments.value = try cast(jsValues: arguments.value, forFunction: self, appContext: appContext)
} catch let error as Exception {
callback(.failure(error))
return
} catch {
callback(.failure(UnexpectedException(error)))
return
}
// Switch from the synchronous context to asynchronous
Task { [arguments] in
let result: Result<Any, Exception>
do {
// Convert arguments to the types desired by the function.
let finalArguments = NonisolatedUnsafeVar<[Any]>([])
if requiresMainActor {
try await MainActor.run {
finalArguments.value = try cast(arguments: arguments.value, forFunction: self, appContext: appContext)
}
} else {
finalArguments.value = try cast(arguments: arguments.value, forFunction: self, appContext: appContext)
}
// TODO: Right now we force cast the tuple in all types of functions, but we should throw another exception here.
// swiftlint:disable force_cast
let argumentsTuple = try Conversions.toTuple(finalArguments.value) as! Args
let returnValue = try await body(argumentsTuple)
result = .success(returnValue)
} catch let error as Exception {
result = .failure(FunctionCallException(name).causedBy(error))
} catch {
result = .failure(UnexpectedException(error))
}
// Go back to the JS thread to execute the callback
try appContext.runtime.schedule {
callback(result)
}
}
}
// MARK: - JavaScriptObjectBuilder
@JavaScriptActor
func build(appContext: AppContext) throws -> JavaScriptObject {
return try appContext.runtime.createAsyncFunction(name, argsCount: argumentsCount) { [weak appContext, self] this, args, resolve, reject in
guard let appContext else {
let exception = Exceptions.AppContextLost()
return reject(exception.code, exception.description, nil)
}
self.call(by: this, withArguments: args, appContext: appContext) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, nil)
case .success(let value):
// Convert some results to primitive types (e.g. records) or JS values (e.g. shared objects)
let convertedResult = Conversions.convertFunctionResult(value, appContext: appContext, dynamicType: ~ReturnType.self)
resolve(convertedResult)
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2025-present 650 Industries. All rights reserved.
/**
A protocol for static any type-erased function.
*/
internal protocol AnyStaticFunctionDefinition: AnyFunctionDefinition {
/**
Indicates whether the function is static.
*/
var isStatic: Bool { get }
}
/**
Represents a static function that can only be called synchronously.
*/
public final class StaticSyncFunctionDefinition<Args, FirstArgType, ReturnType>:
SyncFunctionDefinition<Args, FirstArgType, ReturnType>, AnyStaticFunctionDefinition, @unchecked Sendable {
let isStatic = true
override func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext) throws -> Any {
return try super.call(by: nil, withArguments: args, appContext: appContext)
}
override func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> Void) {
return super.call(by: nil, withArguments: args, appContext: appContext, callback: callback)
}
}
/**
Represents a static function that can only be called asynchronously, thus its JavaScript equivalent returns a Promise.
*/
public final class StaticAsyncFunctionDefinition<Args, FirstArgType, ReturnType>:
AsyncFunctionDefinition<Args, FirstArgType, ReturnType>, AnyStaticFunctionDefinition, @unchecked Sendable {
let isStatic = true
override func call(
by owner: AnyObject?,
withArguments args: [Any],
appContext: AppContext,
callback: @Sendable @escaping (FunctionCallResult) -> Void
) {
return super.call(by: nil, withArguments: args, appContext: appContext, callback: callback)
}
}

View File

@@ -0,0 +1,158 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Type-erased protocol for synchronous functions.
*/
internal protocol AnySyncFunctionDefinition: AnyFunctionDefinition {
/**
Calls the function synchronously with given arguments.
- Parameters:
- owner: An object that calls this function. If the `takesOwner` property is true
and type of the first argument matches the owner type, it's being passed as the argument.
- args: An array of arguments to pass to the function. The arguments must be of the same type as in the underlying closure.
- appContext: An app context where the function is executed.
- Returns: A value returned by the called function when succeeded or an error when it failed.
*/
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext) throws -> Any
/**
Calls the function synchronously with given `this` and arguments as JavaScript values.
It **must** be run on the thread used by the JavaScript runtime.
*/
@discardableResult
func call(_ appContext: AppContext, withThis this: JavaScriptValue?, arguments: [JavaScriptValue]) throws -> JavaScriptValue
}
/**
Represents a function that can only be called synchronously.
*/
public class SyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnySyncFunctionDefinition, @unchecked Sendable {
typealias ClosureType = (Args) throws -> ReturnType
/**
The underlying closure to run when the function is called.
*/
let body: ClosureType
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
returnType: AnyDynamicType = ~ReturnType.self,
_ body: @escaping ClosureType
) {
self.name = name
self.dynamicArgumentTypes = dynamicArgumentTypes
self.returnType = returnType
self.body = body
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
let returnType: AnyDynamicType
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0)
}
var takesOwner: Bool = false
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> Void) {
do {
let result = try call(by: owner, withArguments: args, appContext: appContext)
callback(.success(Conversions.convertFunctionResult(result, appContext: appContext, dynamicType: ~ReturnType.self)))
} catch let error as Exception {
callback(.failure(error))
} catch {
callback(.failure(UnexpectedException(error)))
}
}
// MARK: - AnySyncFunctionDefinition
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext) throws -> Any {
do {
try validateArgumentsNumber(function: self, received: args.count)
var arguments = concat(
arguments: args,
withOwner: owner,
withPromise: nil,
forFunction: self,
appContext: appContext
)
// Convert JS values to non-JS native types.
arguments = try cast(jsValues: arguments, forFunction: self, appContext: appContext)
// Convert arguments to the types desired by the function.
arguments = try cast(arguments: arguments, forFunction: self, appContext: appContext)
guard let argumentsTuple = try Conversions.toTuple(arguments) as? Args else {
throw ArgumentConversionException()
}
return try body(argumentsTuple)
} catch let error as Exception {
throw FunctionCallException(name).causedBy(error)
} catch {
throw UnexpectedException(error)
}
}
func call(_ appContext: AppContext, withThis this: JavaScriptValue?, arguments: [JavaScriptValue]) throws -> JavaScriptValue {
do {
try validateArgumentsNumber(function: self, received: arguments.count)
// This array will include the owner (if needed) and function arguments.
var allNativeArguments: [Any] = []
// If the function takes the owner, convert it and add to the final arguments.
if takesOwner, let this, let ownerType = dynamicArgumentTypes.first {
let nativeOwner = try appContext.converter.toNative(this, ownerType)
allNativeArguments.append(nativeOwner)
}
// Convert JS values to non-JS native types desired by the function.
let nativeArguments = try appContext.converter.toNative(arguments, Array(dynamicArgumentTypes.dropFirst(allNativeArguments.count)))
allNativeArguments.append(contentsOf: nativeArguments)
// Fill in with nils in place of missing optional arguments.
if arguments.count < argumentsCount {
allNativeArguments.append(contentsOf: Array(repeating: Any?.none as Any, count: argumentsCount - arguments.count))
}
guard let argumentsTuple = try Conversions.toTuple(allNativeArguments) as? Args else {
throw ArgumentConversionException()
}
let result = try body(argumentsTuple)
return try appContext.converter.toJS(result, returnType)
} catch let error as Exception {
throw FunctionCallException(name).causedBy(error)
} catch {
throw UnexpectedException(error)
}
}
// MARK: - JavaScriptObjectBuilder
@JavaScriptActor
func build(appContext: AppContext) throws -> JavaScriptObject {
// We intentionally capture a strong reference to `self`, otherwise the "detached" objects would
// immediately lose the reference to the definition and thus the underlying native function.
// It may potentially cause memory leaks, but at the time of writing this comment,
// the native definition instance deallocates correctly when the JS VM triggers the garbage collector.
return try appContext.runtime.createSyncFunction(name, argsCount: argumentsCount) { [weak appContext, self] this, arguments in
guard let appContext else {
throw Exceptions.AppContextLost()
}
return try self.call(appContext, withThis: this, arguments: arguments)
}
}
}

View File

@@ -0,0 +1,201 @@
// Copyright 2025-present 650 Industries. All rights reserved.
/**
Encodes `Encodable` objects or values to `JavaScriptValue`. This implementation is incomplete,
but it supports basic use cases with structs defined by the user and when the default `Encodable` implementation is used.
*/
internal final class JSValueEncoder: Encoder {
private let runtime: JavaScriptRuntime
private let valueHolder = JSValueHolder()
/**
The result of encoding to `JavaScriptValue`. Use this property after running `encode(to:)` on the encodable.
*/
var value: JavaScriptValue {
return valueHolder.value
}
/**
Initializes the encoder with the given runtime in which the value will be created.
*/
init(runtime: JavaScriptRuntime) {
self.runtime = runtime
}
// MARK: - Encoder
// We don't use `codingPath` and `userInfo`, but they are required by the protocol.
let codingPath: [any CodingKey] = []
let userInfo: [CodingUserInfoKey: Any] = [:]
/**
Returns an encoding container appropriate for holding multiple values keyed by the given key type.
*/
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
let container = JSObjectEncodingContainer<Key>(to: valueHolder, runtime: runtime)
return KeyedEncodingContainer(container)
}
/**
Returns an encoding container appropriate for holding multiple unkeyed values.
*/
func unkeyedContainer() -> any UnkeyedEncodingContainer {
return JSArrayEncodingContainer(to: valueHolder, runtime: runtime)
}
/**
Returns an encoding container appropriate for holding a single primitive value, including optionals.
*/
func singleValueContainer() -> any SingleValueEncodingContainer {
return JSValueEncodingContainer(to: valueHolder, runtime: runtime)
}
}
/**
An object that holds a JS value that could be overriden by the encoding container.
*/
private final class JSValueHolder {
var value: JavaScriptValue = .undefined
}
/**
Single value container used to encode primitive values, including optionals.
*/
private struct JSValueEncodingContainer: SingleValueEncodingContainer {
private weak var runtime: JavaScriptRuntime?
private let valueHolder: JSValueHolder
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime?) {
self.runtime = runtime
self.valueHolder = valueHolder
}
// MARK: - SingleValueEncodingContainer
// Unused, but required by the protocol.
let codingPath: [any CodingKey] = []
mutating func encodeNil() throws {
self.valueHolder.value = .null
}
mutating func encode<ValueType: Encodable>(_ value: ValueType) throws {
guard let runtime else {
// Do nothing when the runtime is already deallocated
return
}
let jsValue = JavaScriptValue.from(value, runtime: runtime)
// If the given value couldn't be converted to JavaScriptValue, try to encode it farther.
// It might be the case when the default implementation of `Encodable` has chosen the single value container
// for an optional type that should rather use keyed or unkeyed container when unwrapped.
if jsValue.isUndefined() {
let encoder = JSValueEncoder(runtime: runtime)
try value.encode(to: encoder)
self.valueHolder.value = encoder.value
return
}
self.valueHolder.value = jsValue
}
}
/**
Keyed container that encodes to a JavaScript object.
*/
private struct JSObjectEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
private weak var runtime: JavaScriptRuntime?
private let valueHolder: JSValueHolder
private var object: JavaScriptObject
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime) {
let object = runtime.createObject()
valueHolder.value = JavaScriptValue.from(object, runtime: runtime)
self.runtime = runtime
self.object = object
self.valueHolder = valueHolder
}
// MARK: - KeyedEncodingContainerProtocol
// Unused, but required by the protocol.
var codingPath: [any CodingKey] = []
mutating func encodeNil(forKey key: Key) throws {
object.setProperty(key.stringValue, value: JavaScriptValue.null)
}
mutating func encode<ValueType: Encodable>(_ value: ValueType, forKey key: Key) throws {
guard let runtime else {
// Do nothing when the runtime is already deallocated
return
}
let encoder = JSValueEncoder(runtime: runtime)
try value.encode(to: encoder)
object.setProperty(key.stringValue, value: encoder.value)
}
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
fatalError("JSValueEncoder does not support nested containers")
}
mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
fatalError("JSValueEncoder does not support nested containers")
}
mutating func superEncoder() -> any Encoder {
fatalError("superEncoder() is not implemented in JSValueEncoder")
}
mutating func superEncoder(forKey key: Key) -> any Encoder {
return self.superEncoder()
}
}
/**
Unkeyed container that encodes values to a JavaScript array.
*/
private struct JSArrayEncodingContainer: UnkeyedEncodingContainer {
private weak var runtime: JavaScriptRuntime?
private let valueHolder: JSValueHolder
private var items: [JavaScriptValue] = []
init(to valueHolder: JSValueHolder, runtime: JavaScriptRuntime) {
self.runtime = runtime
self.valueHolder = valueHolder
}
// MARK: - UnkeyedEncodingContainer
// Unused, but required by the protocol.
var codingPath: [any CodingKey] = []
var count: Int = 0
mutating func encodeNil() throws {
items.append(.null)
}
mutating func encode<ValueType: Encodable>(_ value: ValueType) throws {
guard let runtime else {
// Do nothing when the runtime is already deallocated
return
}
let encoder = JSValueEncoder(runtime: runtime)
try value.encode(to: encoder)
items.append(encoder.value)
valueHolder.value = .from(items, runtime: runtime)
}
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
fatalError("JSValueEncoder does not support nested containers")
}
mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
fatalError("JSValueEncoder does not support nested containers")
}
mutating func superEncoder() -> any Encoder {
fatalError("superEncoder() is not implemented in JSValueEncoder")
}
}

View File

@@ -0,0 +1,68 @@
// Copyright 2023-present 650 Industries. All rights reserved.
/**
Represents a JavaScript function that can be called by the native code and that must return the given generic `ReturnType`.
*/
public final class JavaScriptFunction<ReturnType>: AnyArgument, AnyJavaScriptValue, @unchecked Sendable {
/**
Raw representation of the JavaScript function that doesn't impose any restrictions on the returned type.
*/
private let rawFunction: RawJavaScriptFunction
/**
Weak reference to the app context that is necessary to convert some arguments associated with the context (e.g. shared objects).
*/
private weak var appContext: AppContext?
init(rawFunction: RawJavaScriptFunction, appContext: AppContext) {
self.rawFunction = rawFunction
self.appContext = appContext
}
// MARK: - Calling
/**
Calls the function with the given `this` object and arguments.
*/
public func call(_ arguments: Any..., usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
return try call(withArguments: arguments, asConstructor: false, usingThis: this)
}
/**
Calls the function as a constructor with the given arguments. It's like calling a function with the `new` keyword.
*/
public func callAsConstructor(_ arguments: Any...) throws -> ReturnType {
return try call(withArguments: arguments, asConstructor: true, usingThis: nil)
}
/**
Universal function that calls the function with given arguments, this object and whether to call it as a constructor.
*/
private func call(withArguments arguments: [Any] = [], asConstructor: Bool = false, usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
guard let appContext else {
throw Exceptions.AppContextLost()
}
let value = rawFunction.call(withArguments: arguments, thisObject: this, asConstructor: false)
let dynamicType = ~ReturnType.self
guard let result = try appContext.converter.toNative(value, dynamicType) as? ReturnType else {
throw UnexpectedReturnType(dynamicType.description)
}
return result
}
// MARK: - AnyJavaScriptValue
public static func convert(from value: JavaScriptValue, appContext: AppContext) throws -> Self {
guard value.kind == .function else {
throw Conversions.ConvertingException<JavaScriptFunction<ReturnType>>(value)
}
return Self(rawFunction: value.getFunction(), appContext: appContext)
}
}
private final class UnexpectedReturnType: GenericException<String> {
override var reason: String {
return "The function returned a value that cannot be converted to \(param)"
}
}

View File

@@ -0,0 +1,137 @@
// Copyright 2022-present 650 Industries. All rights reserved.
// MARK: - Arguments
/**
Tries to cast a given value to the type that is wrapped by the dynamic type.
- Parameters:
- value: A value to be cast. If it's a ``JavaScriptValue``, it's first unpacked to the raw value.
- type: Something that implements ``AnyDynamicType`` and knows how to cast the argument.
- Returns: A new value converted according to the dynamic type.
- Throws: Rethrows various exceptions that could be thrown by the dynamic types.
*/
internal func cast(_ value: Any, toType type: AnyDynamicType, appContext: AppContext) throws -> Any {
if let dynamicJSType = type as? DynamicJavaScriptType, dynamicJSType.equals(~JavaScriptValue.self) {
return value
}
if !(type is DynamicTypedArrayType), let value = value as? JavaScriptValue {
return try type.cast(value.getRaw(), appContext: appContext)
}
return try type.cast(value, appContext: appContext)
}
/**
Tries to cast the given arguments to the types expected by the function.
- Parameters:
- arguments: An array of arguments to be cast.
- function: A function for which to cast the arguments.
- appContext: A context of the app.
- Returns: An array of arguments after casting. Its size is the same as the input arrays.
- Throws: `InvalidArgsNumberException` when the number of arguments is not equal to the actual number
of function's arguments (without an owner and promise). Rethrows exceptions thrown by `cast(_:toType:)`.
*/
internal func cast(arguments: [Any], forFunction function: AnyFunctionDefinition, appContext: AppContext) throws -> [Any] {
return try arguments.enumerated().map { index, argument in
let argumentType = function.dynamicArgumentTypes[index]
do {
return try cast(argument, toType: argumentType, appContext: appContext)
} catch {
throw ArgumentCastException((index: index, type: argumentType)).causedBy(error)
}
}
}
/**
Casts an array of JavaScript values to non-JavaScript types.
*/
internal func cast(jsValues: [Any], forFunction function: AnyFunctionDefinition, appContext: AppContext) throws -> [Any] {
// TODO: Replace `[Any]` with `[JavaScriptValue]` once we make sure only JS values are passed here
return try jsValues.enumerated().map { index, jsValue in
let type = function.dynamicArgumentTypes[index]
do {
// Temporarily some values might already be cast to primitive types, so make sure we cast only `JavaScriptValue` and leave the others as they are.
if let jsValue = jsValue as? JavaScriptValue {
return try type.cast(jsValue: jsValue, appContext: appContext)
}
return jsValue
} catch {
throw ArgumentCastException((index: index, type: type)).causedBy(error)
}
}
}
/**
Validates whether the number of received arguments is enough to call the given function.
Throws `InvalidArgsNumberException` otherwise.
*/
internal func validateArgumentsNumber(function: AnyFunctionDefinition, received: Int) throws {
let argumentsCount = function.argumentsCount
let requiredArgumentsCount = function.requiredArgumentsCount
if received < requiredArgumentsCount || received > argumentsCount {
throw InvalidArgsNumberException((
received: received,
expected: argumentsCount,
required: requiredArgumentsCount
))
}
}
/**
Ensures the provided array of arguments matches the number of arguments expected by the function.
- If the function takes the owner, it's added to the beginning.
- If the array is still too small, missing arguments are very likely to be optional so it puts `nil` in their place.
*/
internal func concat(
arguments: [Any],
withOwner owner: AnyObject?,
withPromise promise: Promise?,
forFunction function: AnyFunctionDefinition,
appContext: AppContext
) -> [Any] {
var result = arguments
if function.takesOwner {
result = [owner as Any] + arguments
}
if arguments.count < function.argumentsCount {
result += Array(repeating: Any?.none as Any, count: function.argumentsCount - arguments.count)
}
// Add promise to the array of arguments if necessary.
if let promise {
result += [promise]
}
return result
}
// MARK: - Exceptions
internal final class InvalidArgsNumberException: GenericException<(received: Int, expected: Int, required: Int)>, @unchecked Sendable {
override var reason: String {
if param.required < param.expected {
return "Received \(param.received) arguments, but \(param.expected) was expected and at least \(param.required) is required"
}
return "Received \(param.received) arguments, but \(param.expected) was expected"
}
}
internal final class ArgumentCastException: GenericException<(index: Int, type: AnyDynamicType)>, @unchecked Sendable {
override var reason: String {
"The \(formatOrdinalNumber(param.index + 1)) argument cannot be cast to type \(param.type.description)"
}
func formatOrdinalNumber(_ number: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
formatter.locale = Locale(identifier: "en_US")
return formatter.string(from: NSNumber(value: number)) ?? ""
}
}
private final class ModuleUnavailableException: GenericException<String>, @unchecked Sendable {
override var reason: String {
"Module '\(param)' is no longer available"
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import os.log
public func createOSLogHandler(category: String) -> LogHandler {
if #available(iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
return OSLogHandler(category: category)
}
return PrintLogHandler()
}
public func createPersistentFileLogHandler(category: String) -> LogHandler {
return PersistentFileLogHandler(category: category)
}
/**
The protocol that needs to be implemented by log handlers.
*/
public protocol LogHandler {
func log(type: LogType, _ message: String)
}
/**
The log handler that uses the new `os.Logger` API.
*/
@available(iOS 14.0, watchOS 7.0, tvOS 14.0, *)
internal class OSLogHandler: LogHandler {
private let osLogger: os.Logger
required init(category: String) {
osLogger = os.Logger(subsystem: Logger.EXPO_MODULES_LOG_SUBSYSTEM, category: category)
}
func log(type: LogType, _ message: String) {
osLogger.log(level: type.toOSLogType(), "\(message)")
}
}
/**
Simple log handler that forwards all logs to `print` function.
*/
internal class PrintLogHandler: LogHandler {
func log(type: LogType, _ message: String) {
print(message)
}
}
/**
Log handler that writes all logs to a file using PersistentFileLog
*/
internal class PersistentFileLogHandler: LogHandler {
private let persistentLog: PersistentFileLog
required init(category: String) {
self.persistentLog = PersistentFileLog(category: category)
}
func log(type: LogType, _ message: String) {
persistentLog.appendEntry(entry: message)
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import os.log
/**
An enum with available log types.
*/
public enum LogType: Int {
case trace = 0
case timer = 1
case stacktrace = 2
case debug = 3
case info = 4
case warn = 5
case error = 6
case fatal = 7
/**
The string that is used to prefix the messages of this log type.
Logs in Xcode and Console apps are always with the white text,
so we use colored circle emojis to distinguish different types of logs.
*/
var prefix: String {
switch self {
case .trace:
return "⚪️"
case .timer:
return "🟤"
case .stacktrace:
return "🟣"
case .debug:
return "🔵"
case .info:
return "🟢"
case .warn:
return "🟡"
case .error:
return "🟠"
case .fatal:
return "🔴"
}
}
/**
Maps the log types to the log types used by the `os.log` logger.
*/
@available(iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public func toOSLogType() -> OSLogType {
switch self {
case .trace, .timer, .stacktrace, .debug:
return .debug
case .info:
return .info
case .warn:
return .default
case .error:
return .error
case .fatal:
return .fault
}
}
}

View File

@@ -0,0 +1,159 @@
// Copyright 2021-present 650 Industries. All rights reserved.
import Dispatch
public let log = Logger(logHandlers: [createOSLogHandler(category: Logger.EXPO_LOG_CATEGORY)])
public typealias LoggerTimerFormatterBlock = (_ duration: Double) -> String
/**
Native iOS logging class for Expo, with options to direct logs
to different destinations.
*/
public class Logger: @unchecked Sendable {
public static let EXPO_MODULES_LOG_SUBSYSTEM = "dev.expo.modules"
public static let EXPO_LOG_CATEGORY = "expo"
#if DEBUG || EXPO_CONFIGURATION_DEBUG
private var minLevel: LogType = .trace
#else
private var minLevel: LogType = .info
#endif
private let logHandlers: [LogHandler]
public required init(logHandlers: [LogHandler]) {
self.logHandlers = logHandlers
}
// MARK: - Public logging functions
/**
The most verbose log level that captures all the details about the behavior of the implementation.
It is mostly diagnostic and is more granular and finer than `debug` log level.
These logs should not be committed to the repository and are ignored in the release builds.
*/
public func trace(_ items: Any...) {
log(type: .trace, items)
}
/**
Used to log diagnostically helpful information. As opposed to `trace`,
it is acceptable to commit these logs to the repository. Ignored in the release builds.
*/
public func debug(_ items: Any...) {
log(type: .debug, items)
}
/**
For information that should be logged under normal conditions such as successful initialization
and notable events that are not considered an error but might be useful for debugging purposes in the release builds.
*/
public func info(_ items: Any...) {
log(type: .info, items)
}
/**
Used to log an unwanted state that has not much impact on the process so it can be continued, but could potentially become an error.
*/
public func warn(_ items: Any...) {
log(type: .warn, items)
}
/**
Logs unwanted state that has an impact on the currently running process, but the entire app can continue to run.
*/
public func error(_ items: Any...) {
log(type: .error, items)
}
/**
Logs critical error due to which the entire app cannot continue to run.
*/
public func fatal(_ items: Any...) {
log(type: .fatal, items)
}
/**
Logs the stack of symbols on the current thread.
*/
public func stacktrace(type: LogType = .stacktrace, file: String = #fileID, line: UInt = #line) {
guard type.rawValue >= minLevel.rawValue else {
return
}
let queueName = OperationQueue.current?.underlyingQueue?.label ?? "<unknown>"
// Get the call stack symbols without the first symbol as it points right here.
let symbols = Thread.callStackSymbols.dropFirst()
log(type: type, "The stacktrace from '\(file):\(line)' on queue '\(queueName)':")
symbols.forEach { symbol in
let formattedSymbol = reformatStackSymbol(symbol)
log(type: type, "\(formattedSymbol)")
}
}
/**
Allows the logger instance to be called as a function. The same as `logger.debug(...)`.
*/
public func callAsFunction(_ items: Any...) {
log(type: .debug, items)
}
// MARK: - Timers
/**
Starts a timer that can be used to compute the duration of an operation. Upon calling
`stop` on the returned object, a timer entry will be logged.
*/
public func startTimer(_ formatterBlock: @escaping LoggerTimerFormatterBlock) -> LoggerTimer {
let startTime = DispatchTime.now()
return LoggerTimer {
let endTime = DispatchTime.now()
let diffMs = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000
self.log(type: .timer, formatterBlock(diffMs))
return diffMs
}
}
// MARK: - Private logging functions
private func log(type: LogType = .trace, _ items: [Any]) {
guard type.rawValue >= minLevel.rawValue else {
return
}
let messages = items
.map { describe(value: $0) }
.joined(separator: " ")
.split(whereSeparator: \.isNewline)
.map { "\(type.prefix) \($0)" }
logHandlers.forEach { handler in
messages.forEach { message in
handler.log(type: type, message)
}
}
}
private func log(type: LogType = .trace, _ items: Any...) {
log(type: type, items)
}
}
private func reformatStackSymbol(_ symbol: String) -> String {
return symbol.replacingOccurrences(of: #"^\d+\s+"#, with: "", options: .regularExpression)
}
private func describe(value: Any) -> String {
if let value = value as? String {
return value
}
if let value = value as? CustomDebugStringConvertible {
return value.debugDescription
}
if let value = value as? CustomStringConvertible {
return value.description
}
return String(describing: value)
}

View File

@@ -0,0 +1,22 @@
// Copyright 2021-present 650 Industries. All rights reserved.
import Foundation
typealias LoggerTimerStopBlock = () -> Double
/**
An instance of a timer.
*/
public class LoggerTimer {
private let stopBlock: LoggerTimerStopBlock
internal required init(stopBlock: @escaping LoggerTimerStopBlock) {
self.stopBlock = stopBlock
}
/**
End the timer and log a timer entry. Returns the duration in milliseconds.
*/
public func stop() -> Double {
return self.stopBlock()
}
}

View File

@@ -0,0 +1,159 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import Foundation
public typealias PersistentFileLogFilter = (String) -> Bool
public typealias PersistentFileLogCompletionHandler = (Error?) -> Void
/**
* A thread-safe class for reading and writing line-separated strings to a flat file.
* The main use case is for logging specific errors or events, and ensuring that the
* logs persist across application crashes and restarts (for example, OSLogReader can
* only read system logs for the current process, and cannot access anything logged
* before the current process started).
*
* All write access to the file goes through asynchronous public methods managed by a
* serial dispatch queue.
*
* The dispatch queue is global, to ensure that multiple instances accessing the same
* file will have thread-safe access.
*
* The only operations supported are
* - Read the file (synchronous)
* - Append one or more entries to the file
* - Filter the file (only retain entries that pass the filter check)
* - Clear the file (remove all entries)
*
*/
@preconcurrency
public class PersistentFileLog {
private static let EXPO_MODULES_CORE_LOG_QUEUE_LABEL = "dev.expo.modules.core.logging"
private static let serialQueue = DispatchQueue(label: EXPO_MODULES_CORE_LOG_QUEUE_LABEL)
private let category: String
private let filePath: String
public init(category: String) {
self.category = category
let fileName = "\(PersistentFileLog.EXPO_MODULES_CORE_LOG_QUEUE_LABEL).\(category).txt"
// Execution aborts if no application support directory
self.filePath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName).path
}
/**
Read entries from log file
*/
public func readEntries() -> [String] {
if getFileSize() == 0 {
return []
}
return (try? self.readFileSync()) ?? []
}
/**
Append entry to the log file
Since logging may not require a result handler, the handler parameter is optional
If no error handler provided, print error to the console
*/
public func appendEntry(entry: String, _ completionHandler: PersistentFileLogCompletionHandler? = nil) {
PersistentFileLog.serialQueue.async {
self.ensureFileExists()
do {
try self.appendTextToFile(text: entry + "\n")
completionHandler?(nil)
} catch {
completionHandler?(error)
}
}
}
/**
Filter existing entries and remove ones where filter(entry) == false
*/
public func purgeEntriesNotMatchingFilter(filter: @escaping PersistentFileLogFilter, _ completionHandler: @escaping PersistentFileLogCompletionHandler) {
PersistentFileLog.serialQueue.async {
self.ensureFileExists()
do {
let contents = try self.readFileSync()
let newcontents = contents.filter { entry in filter(entry) }
try self.writeFileSync(newcontents)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
/**
Clean up (remove) the log file
*/
public func clearEntries(_ completionHandler: @escaping PersistentFileLogCompletionHandler) {
PersistentFileLog.serialQueue.async {
do {
try self.deleteFileSync()
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
// MARK: - Private methods
private func ensureFileExists() {
if !FileManager.default.fileExists(atPath: filePath) {
FileManager.default.createFile(atPath: filePath, contents: nil)
}
}
private func getFileSize() -> Int {
// Gets the file size, or returns 0 if the file does not exist
do {
let attrs: [FileAttributeKey: Any?] = try FileManager.default.attributesOfItem(atPath: filePath)
return attrs[FileAttributeKey.size] as? Int ?? 0
} catch {
return 0
}
}
private func appendTextToFile(text: String) throws {
if let data = text.data(using: .utf8) {
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
try EXUtilities.catchException {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
}
}
}
private func readFileSync() throws -> [String] {
return try stringToList(String(contentsOfFile: filePath, encoding: .utf8))
}
private func writeFileSync(_ contents: [String]) throws {
if contents.isEmpty {
try deleteFileSync()
return
}
try contents.joined(separator: "\n").write(toFile: filePath, atomically: true, encoding: .utf8)
}
private func deleteFileSync() throws {
if FileManager.default.fileExists(atPath: filePath) {
try FileManager.default.removeItem(atPath: filePath)
}
}
private func stringToList(_ contents: String?) -> [String] {
// If null contents, or 0 length contents, return empty list
guard let contents = contents, !contents.isEmpty else {
return []
}
return contents
.components(separatedBy: "\n")
.filter {entryString in
!entryString.isEmpty
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright 2024-present 650 Industries. All rights reserved.
/**
A converter associated with the specific app context that delegates value conversions to the dynamic type converters.
*/
public struct MainValueConverter {
private(set) weak var appContext: AppContext?
/**
Casts the given JavaScriptValue to a non-JS value.
It **must** be run on the thread used by the JavaScript runtime.
*/
public func toNative(_ value: JavaScriptValue, _ type: AnyDynamicType) throws -> Any {
// Preliminary cast from JS value to a common native type.
guard let appContext else {
throw Exceptions.AppContextLost()
}
let rawValue = try type.cast(jsValue: value, appContext: appContext)
// Cast common native type to more complex types (e.g. records, convertibles, enumerables, shared objects).
return try type.cast(rawValue, appContext: appContext)
}
/**
Casts the given JS values to non-JS values.
It **must** be run on the thread used by the JavaScript runtime.
*/
public func toNative(_ values: [JavaScriptValue], _ types: [AnyDynamicType]) throws -> [Any] {
// While using `values.enumerated().map` sounds like a more straightforward approach,
// this code seems quite critical for performance and using a standard `map` performs much better.
var index = 0
return try values.map { value in
let type = types[index]
index += 1
do {
return try toNative(value, type)
} catch {
throw ArgumentCastException((index: index - 1, type: type)).causedBy(error)
}
}
}
/**
Converts the given value to the type compatible with JavaScript.
*/
public func toJS<ValueType>(_ value: ValueType, _ type: AnyDynamicType) throws -> JavaScriptValue {
guard let appContext else {
throw Exceptions.AppContextLost()
}
let result = Conversions.convertFunctionResult(value, appContext: appContext, dynamicType: type)
return try type.castToJS(result, appContext: appContext)
}
}

View File

@@ -0,0 +1,157 @@
import Dispatch
/**
Holds a reference to the module instance and caches its definition.
*/
public final class ModuleHolder {
/**
Name of the module.
*/
private(set) var _name: String?
/**
Instance of the module.
*/
private(set) var module: AnyModule
/**
A weak reference to the app context.
*/
private(set) weak var appContext: AppContext?
/**
JavaScript object that represents the module instance in the runtime.
*/
@JavaScriptActor
public internal(set) lazy var javaScriptObject: JavaScriptObject? = createJavaScriptModuleObject()
/**
Caches the definition of the module type.
*/
let definition: ModuleDefinition
/**
Returns `definition.name` if not empty, otherwise falls back to the module type name.
*/
var name: String {
return _name ?? (definition.name.isEmpty ? String(describing: type(of: module)) : definition.name)
}
/**
Number of JavaScript listeners attached to the module.
*/
var listenersCount: Int = 0
init(appContext: AppContext, module: AnyModule, name: String?) {
self.appContext = appContext
self._name = name
self.module = module
self.definition = module.definition()
post(event: .moduleCreate)
}
// MARK: Constants
/**
Merges all `constants` definitions into one dictionary.
*/
func getLegacyConstants() -> [String: Any?] {
return definition.getLegacyConstants()
}
// MARK: Calling functions
@preconcurrency
func call(function functionName: String, args: [Any], _ callback: @Sendable @escaping (FunctionCallResult) -> Void = { _ in }) {
guard let appContext else {
callback(.failure(Exceptions.AppContextLost()))
return
}
guard let function = definition.functions[functionName] else {
callback(.failure(FunctionNotFoundException((functionName: functionName, moduleName: self.name))))
return
}
function.call(by: self, withArguments: args, appContext: appContext, callback: callback)
}
@discardableResult
func callSync(function functionName: String, args: [Any]) -> Any? {
guard let appContext, let function = definition.functions[functionName] as? AnySyncFunctionDefinition else {
return nil
}
do {
let arguments = try cast(arguments: args, forFunction: function, appContext: appContext)
let result = try function.call(by: self, withArguments: arguments, appContext: appContext)
if let result = result as? SharedObject {
return appContext.sharedObjectRegistry.ensureSharedJavaScriptObject(runtime: try appContext.runtime, nativeObject: result)
}
return result
} catch {
return error
}
}
// MARK: JavaScript Module Object
/**
Creates the JavaScript object that will be used to communicate with the native module.
The object is prefilled with module's constants and functions.
JavaScript can access it through `global.expo.modules[moduleName]`.
- Note: The object will be `nil` when the runtime is unavailable (e.g. remote debugger is enabled).
*/
@JavaScriptActor
private func createJavaScriptModuleObject() -> JavaScriptObject? {
// It might be impossible to create any object at the moment (e.g. remote debugging, app context destroyed)
guard let appContext else {
return nil
}
do {
log.info("Creating JS object for module '\(name)'")
return try definition.build(appContext: appContext)
} catch {
log.error("Building the module object failed: \(error)")
return nil
}
}
// MARK: Listening to native events
func listeners(forEvent event: EventName) -> [EventListener] {
return definition.eventListeners.filter {
$0.name == event
}
}
func post(event: EventName) {
listeners(forEvent: event).forEach {
try? $0.call(module, nil)
}
}
func post<PayloadType>(event: EventName, payload: PayloadType?) {
listeners(forEvent: event).forEach {
try? $0.call(module, payload)
}
}
// MARK: Deallocation
deinit {
post(event: .moduleDestroy)
}
// MARK: - Exceptions
internal class ModuleNotFoundException: GenericException<String> {
override var reason: String {
"Module '\(param)' not found"
}
}
internal class FunctionNotFoundException: GenericException<(functionName: String, moduleName: String)> {
override var reason: String {
"Function '\(param.functionName)' not found in module '\(param.moduleName)'"
}
}
}

View File

@@ -0,0 +1,120 @@
public final class ModuleRegistry: Sequence {
public typealias Element = ModuleHolder
private weak var appContext: AppContext?
private var registry: [String: ModuleHolder] = [:]
private var overrideDisallowModules = Set<String>()
init(appContext: AppContext) {
self.appContext = appContext
}
/**
Registers an instance of module holder.
*/
internal func register(holder: ModuleHolder, preventModuleOverriding: Bool = false) {
log.info("Registering module '\(holder.name)'")
// if overriding is disallowed for this module and the module already registered, don't re-register
if overrideDisallowModules.contains(holder.name) && registry[holder.name] != nil {
log.info("Not re-registering module '\(holder.name)' since a previous registration specified preventModuleOverriding: true")
return
}
if preventModuleOverriding {
overrideDisallowModules.insert(holder.name)
}
registry[holder.name] = holder
}
/**
Registers an instance of the module.
*/
public func register(module: AnyModule, name: String?, preventModuleOverriding: Bool = false) {
guard let appContext else {
log.error("Unable to register a module '\(module)', the app context is unavailable")
return
}
register(holder: ModuleHolder(appContext: appContext, module: module, name: name), preventModuleOverriding: preventModuleOverriding)
}
/**
Registers a module by its type.
*/
public func register(moduleType: AnyModule.Type, name: String?, preventModuleOverriding: Bool = false) {
guard let appContext else {
log.error("Unable to register a module '\(moduleType)', the app context is unavailable")
return
}
register(module: moduleType.init(appContext: appContext), name: name, preventModuleOverriding: preventModuleOverriding)
}
/**
Registers modules exported by given modules provider.
*/
public func register(fromProvider provider: ModulesProviderProtocol) {
provider.getModuleClasses().forEach { module, name in
register(moduleType: module, name: name)
}
}
/**
Unregisters given module from the registry.
*/
public func unregister(module: AnyModule) {
if let index = registry.firstIndex(where: { $1.module === module }) {
registry.remove(at: index)
}
}
public func unregister(moduleName: String) {
if registry[moduleName] != nil {
log.info("Unregistering module '\(moduleName)'")
registry[moduleName] = nil
}
}
public func has(moduleWithName moduleName: String) -> Bool {
return registry[moduleName] != nil
}
public func get(moduleHolderForName moduleName: String) -> ModuleHolder? {
return registry[moduleName]
}
public func get(moduleWithName moduleName: String) -> AnyModule? {
return registry[moduleName]?.module
}
internal func getModule<ModuleProtocol>(implementing protocol: ModuleProtocol.Type) -> ModuleProtocol? {
for holder in registry.values {
if let module = holder.module as? ModuleProtocol {
return module
}
}
return nil
}
public func getModuleNames() -> [String] {
return Array(registry.keys)
}
public func makeIterator() -> IndexingIterator<[ModuleHolder]> {
return registry.map({ $1 }).makeIterator()
}
internal func post(event: EventName) {
log.info("Posting '\(event)' event to registered modules")
forEach { holder in
holder.post(event: event)
}
}
internal func post<PayloadType>(event: EventName, payload: PayloadType? = nil) {
log.info("Posting '\(event)' event to registered modules")
forEach { holder in
holder.post(event: event, payload: payload)
}
}
}

View File

@@ -0,0 +1,134 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import React
import Foundation
private let WORKLET_RUNTIME_KEY = "_WORKLET_RUNTIME"
// The core module that describes the `global.expo` object.
internal final class CoreModule: Module {
internal func definition() -> ModuleDefinition {
Constant("expoModulesCoreVersion") {
let version = CoreModuleHelper.getVersion()
let components = version.split(separator: "-")[0].split(separator: ".").compactMap { Int($0) }
return [
"version": version,
"major": components[0],
"minor": components[1],
"patch": components[2]
]
}
Constant("cacheDir") {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? ""
}
Constant("documentsDir") {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path ?? ""
}
Function("installOnUIRuntime") {
guard let appContext else {
throw Exceptions.AppContextLost()
}
// Check if was already installed
if appContext._uiRuntime != nil {
return
}
let runtime = try appContext.runtime
if !runtime.global().hasProperty(WORKLET_RUNTIME_KEY) {
throw WorkletUIRuntimeException()
}
let pointerHolder = runtime.global().getProperty(WORKLET_RUNTIME_KEY)
if !pointerHolder.isObject() {
throw WorkletUIRuntimeException()
}
let uiRuntimePointer = WorkletRuntimeFactory.extractRuntimePointer(pointerHolder, runtime: runtime)
if uiRuntimePointer == nil {
throw WorkletUIRuntimePointerExtractionException()
}
let block = {
let workletRuntime = WorkletRuntimeFactory.createWorkletRuntime(appContext, fromPointer: uiRuntimePointer)
appContext._uiRuntime = workletRuntime
}
if Thread.isMainThread {
block()
} else {
DispatchQueue.main.sync {
block()
}
}
}
// Expose some common classes and maybe even the `modules` host object in the future.
Function("uuidv4") { () -> String in
return UUID().uuidString.lowercased()
}
Function("uuidv5") { (name: String, namespace: String) -> String in
guard let namespaceUuid = UUID(uuidString: namespace) else {
throw InvalidNamespaceException(namespace)
}
return uuidv5(name: name, namespace: namespaceUuid).uuidString.lowercased()
}
// swiftlint:disable:next unused_closure_parameter
Function("getViewConfig") { (moduleName: String, viewName: String?) -> [String: Any]? in
var validAttributes: [String: Any] = [:]
var directEventTypes: [String: Any] = [:]
let moduleHolder = appContext?.moduleRegistry.get(moduleHolderForName: getHolderName(moduleName))
guard let viewDefinition = moduleHolder?.definition.views[viewName ?? DEFAULT_MODULE_VIEW] else {
return nil
}
for propName in viewDefinition.getSupportedPropNames() {
validAttributes[propName] = true
}
for eventName in viewDefinition.getSupportedEventNames() {
guard let normalizedEventName = RCTNormalizeInputEventName(eventName) else {
continue
}
directEventTypes[normalizedEventName] = [
"registrationName": eventName
]
}
return [
"validAttributes": validAttributes,
"directEventTypes": directEventTypes
]
}
AsyncFunction("reloadAppAsync") { (reason: String) in
appContext?.reloadAppAsync(reason)
}
}
private func getHolderName(_ viewName: String) -> String {
if let appIdentifier = appContext?.appIdentifier, viewName.hasSuffix("_\(appIdentifier)") {
return String(viewName.dropLast("_\(appIdentifier)".count))
}
return viewName
}
}
internal final class WorkletUIRuntimeException: Exception, @unchecked Sendable {
override var reason: String {
"Cannot find UI worklet runtime"
}
}
internal final class WorkletUIRuntimePointerExtractionException: Exception, @unchecked Sendable {
override var reason: String {
"Cannot extract pointer to UI worklet runtime"
}
}

View File

@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
@interface CoreModuleHelper : NSObject
NS_ASSUME_NONNULL_BEGIN
+ (NSString *)getVersion;
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,15 @@
#import "CoreModuleHelper.h"
#ifdef EXPO_MODULES_CORE_VERSION
#define STRINGIZE(x) #x
#define STRINGIZE2(x) STRINGIZE(x)
#define EXPO_MODULES_CORE_VERSION_STRING STRINGIZE2(EXPO_MODULES_CORE_VERSION)
#endif
@implementation CoreModuleHelper
+ (NSString *)getVersion {
return @EXPO_MODULES_CORE_VERSION_STRING;
}
@end

View File

@@ -0,0 +1,65 @@
private let onNewError = "\(JSLoggerModule.name).onNewError"
private let onNewWarning = "\(JSLoggerModule.name).onNewWarning"
private let onNewDebug = "\(JSLoggerModule.name).onNewDebug"
private let onNewInfo = "\(JSLoggerModule.name).onNewInfo"
private let onNewTrace = "\(JSLoggerModule.name).onNewTrace"
public final class JSLoggerModule: Module {
public static let name = "ExpoModulesCoreJSLogger"
// We could have made the JSLoggerModule implement the LogHandler interface, but the Logger
// holds a strong reference to the LogHandlers, which would lead to a reference cycle.
private class JSLogHandler: LogHandler {
weak var module: JSLoggerModule?
init(module: JSLoggerModule) {
self.module = module
}
func log(type: LogType, _ message: String) {
module?.reportToLogBox(type: type, message)
}
}
public var logger: Logger?
public func definition() -> ModuleDefinition {
Name(Self.name)
Events(onNewError, onNewWarning, onNewDebug, onNewInfo, onNewTrace)
OnCreate {
let logHandler = JSLogHandler(module: self)
self.logger = Logger(logHandlers: [logHandler])
}
}
private func reportToLogBox(type: LogType, _ message: String) {
self.sendEvent(type.eventName, [
"message": message
])
}
}
private extension LogType {
var eventName: String {
switch self {
case .trace:
onNewTrace
case .timer:
onNewDebug
case .stacktrace:
onNewTrace
case .debug:
onNewDebug
case .info:
onNewInfo
case .warn:
onNewWarning
case .error:
onNewError
case .fatal:
onNewError
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
`BaseModule` is just a stub class that fulfils `AnyModule` protocol requirement of public default initializer,
but doesn't implement that protocol explicitly, though  it would have to provide a definition which would require
other modules to use `override` keyword in the function returning the definition.
*/
open class BaseModule {
public private(set) weak var appContext: AppContext?
@available(*, unavailable, message: "Module's initializer cannot be overriden, use \"onCreate\" definition component instead.")
public init() {}
required public init(appContext: AppContext) {
self.appContext = appContext
}
/**
Sends an event with given name and body to JavaScript.
*/
public func sendEvent(_ eventName: String, _ body: [String: Any?] = [:]) {
appContext?.eventEmitter?.sendEvent(withName: eventName, body: body)
}
}
/**
An alias for `AnyModule` extended by the `BaseModule` class that provides public default initializer.
*/
public typealias Module = AnyModule & BaseModule

View File

@@ -0,0 +1,118 @@
let DEFAULT_MODULE_VIEW = "DEFAULT_MODULE_VIEW"
/**
The definition of the module. It is used to define some parameters
of the module and what it exports to the JavaScript world.
See `ModuleDefinitionBuilder` for more details on how to create it.
*/
public final class ModuleDefinition: ObjectDefinition {
/**
The module's type associated with the definition. It's used to create the module instance.
*/
var type: AnyModule.Type?
/**
Name of the defined module. Falls back to the type name if not provided in the definition.
*/
var name: String
let eventListeners: [EventListener]
let views: [String: AnyViewDefinition]
/**
Names of the events that the module can send to JavaScript.
*/
let eventNames: [String]
let eventObservers: [AnyEventObservingDefinition]
/**
Initializer that is called by the `ModuleDefinitionBuilder` results builder.
*/
override init(definitions: [AnyDefinition]) {
self.name = definitions
.compactMap { $0 as? ModuleNameDefinition }
.last?
.name ?? ""
self.eventListeners = definitions.compactMap { $0 as? EventListener }
let viewDefinitions: [AnyViewDefinition] = definitions
.compactMap { $0 as? AnyViewDefinition }
var viewsDict = Dictionary(uniqueKeysWithValues: viewDefinitions.map { ($0.name, $0) })
viewsDict[DEFAULT_MODULE_VIEW] = viewDefinitions.first
self.views = viewsDict
self.eventNames = Array(
definitions
.compactMap { ($0 as? EventsDefinition)?.names }
.joined()
)
self.eventObservers = definitions
.compactMap { $0 as? AnyEventObservingDefinition }
super.init(definitions: definitions)
}
/**
Sets the module type that the definition is associated with. We can't pass this in the initializer
as it's called by the results builder that doesn't have access to the type.
*/
func withType(_ type: AnyModule.Type) -> Self {
self.type = type
// Use the type name if the name is not in the definition or was defined empty.
if name.isEmpty {
name = String(describing: type)
}
return self
}
@JavaScriptActor
public override func build(appContext: AppContext) throws -> JavaScriptObject {
// Create an instance of `global.expo.NativeModule`
let object = JSUtils.createNativeModuleObject(try appContext.runtime)
try super.decorate(object: object, appContext: appContext)
let viewPrototypesObject = try appContext.runtime.createObject()
try views.forEach { key, view in
let reactComponentPrototype = try view.createReactComponentPrototype(appContext: appContext)
viewPrototypesObject.setProperty(key == DEFAULT_MODULE_VIEW ? name : "\(name)_\(view.name)", value: reactComponentPrototype)
}
if !eventObservers.isEmpty {
try EventObservingDecorator(definitions: eventObservers)
.decorate(object: object, appContext: appContext)
}
object.setProperty("ViewPrototypes", value: viewPrototypesObject)
// Give the module object a name. It's used for compatibility reasons, see `EventEmitter.ts`.
object.defineProperty("__expo_module_name__", value: name, options: [])
return object
}
}
/**
Module's name definition. Returned by `name()` in module's definition.
*/
internal struct ModuleNameDefinition: AnyDefinition {
let name: String
}
/**
A definition for module's constants. Returned by `constants(() -> SomeType)` in module's definition.
*/
internal struct ConstantsDefinition: AnyDefinition {
let body: () -> [String: Any?]
}
/**
A definition for module's events that can be sent to JavaScript.
*/
public struct EventsDefinition: AnyDefinition {
let names: [String]
}

View File

@@ -0,0 +1,10 @@
/**
A function builder that provides DSL-like syntax. Thanks to this, the function doesn't need to explicitly return an array,
but can just return multiple definitions one after another. This works similarly to SwiftUI's `body` block.
*/
@resultBuilder
public struct ModuleDefinitionBuilder {
public static func buildBlock(_ definitions: AnyDefinition...) -> ModuleDefinition {
return ModuleDefinition(definitions: definitions)
}
}

Some files were not shown because too many files have changed in this diff Show More