Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion android/src/main/java/com/prisma/PrismaModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,17 @@ public void createDir(File dir) throws IOException
public void install() {
ReactApplicationContext context = this.getReactApplicationContext();
long jsContextPointer = context.getJavaScriptContextHolder().get();
CallInvokerHolderImpl jsCallInvokerHolder = (CallInvokerHolderImpl)context.getCatalystInstance().getJSCallInvokerHolder();
if (jsContextPointer == 0) {
throw new RuntimeException("JSI runtime pointer is null. Make sure Hermes is enabled and the runtime is initialized before calling install().");
}
CallInvokerHolderImpl jsCallInvokerHolder;
if (context.getJSCallInvokerHolder() != null) {
jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder();
} else if (context.getCatalystInstance() != null) {
jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
} else {
throw new RuntimeException("JSCallInvokerHolder is not available yet.");
}
Comment on lines +125 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null-safety improvements are good, but the fallback path may still NPE.

The null checks for jsContextPointer and getJSCallInvokerHolder() are correct. However, on line 132, context.getCatalystInstance().getJSCallInvokerHolder() may also return null even when getCatalystInstance() is non-null, leading to an NPE during the cast.

🛠️ Proposed fix to add null check
     if (context.getJSCallInvokerHolder() != null) {
       jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder();
     } else if (context.getCatalystInstance() != null) {
-      jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
+      var holder = context.getCatalystInstance().getJSCallInvokerHolder();
+      if (holder == null) {
+        throw new RuntimeException("JSCallInvokerHolder from CatalystInstance is null.");
+      }
+      jsCallInvokerHolder = (CallInvokerHolderImpl) holder;
     } else {
       throw new RuntimeException("JSCallInvokerHolder is not available yet.");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (jsContextPointer == 0) {
throw new RuntimeException("JSI runtime pointer is null. Make sure Hermes is enabled and the runtime is initialized before calling install().");
}
CallInvokerHolderImpl jsCallInvokerHolder;
if (context.getJSCallInvokerHolder() != null) {
jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder();
} else if (context.getCatalystInstance() != null) {
jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder();
} else {
throw new RuntimeException("JSCallInvokerHolder is not available yet.");
}
if (jsContextPointer == 0) {
throw new RuntimeException("JSI runtime pointer is null. Make sure Hermes is enabled and the runtime is initialized before calling install().");
}
CallInvokerHolderImpl jsCallInvokerHolder;
if (context.getJSCallInvokerHolder() != null) {
jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder();
} else if (context.getCatalystInstance() != null) {
var holder = context.getCatalystInstance().getJSCallInvokerHolder();
if (holder == null) {
throw new RuntimeException("JSCallInvokerHolder from CatalystInstance is null.");
}
jsCallInvokerHolder = (CallInvokerHolderImpl) holder;
} else {
throw new RuntimeException("JSCallInvokerHolder is not available yet.");
}

String dbPath = context.getDatabasePath("defaultDatabase").getAbsolutePath().replace("defaultDatabase", "");
String migrationsPath;
try {
Expand Down
3 changes: 1 addition & 2 deletions copy-migrations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ echo "Copying prisma migration files..."
MIGRATIONS_TARGET=${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}

rm -rf "$MIGRATIONS_TARGET/migrations"
mkdir "$MIGRATIONS_TARGET/migrations"
cp -r ${SRCROOT}/../migrations ${MIGRATIONS_TARGET}
cp -r ${SRCROOT}/../prisma/migrations "${MIGRATIONS_TARGET}/migrations"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Quote the source path to prevent word splitting.

If SRCROOT contains spaces, this command will fail. Quote the variable expansion for robustness.

🛠️ Proposed fix
-cp -r ${SRCROOT}/../prisma/migrations "${MIGRATIONS_TARGET}/migrations"
+cp -r "${SRCROOT}/../prisma/migrations" "${MIGRATIONS_TARGET}/migrations"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cp -r ${SRCROOT}/../prisma/migrations "${MIGRATIONS_TARGET}/migrations"
cp -r "${SRCROOT}/../prisma/migrations" "${MIGRATIONS_TARGET}/migrations"
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 8-8: Double quote to prevent globbing and word splitting.

(SC2086)


echo "migration files copied ✅"
7 changes: 6 additions & 1 deletion cpp/QueryEngineHostObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
#ifndef query_engine_host_object_h
#define query_engine_host_object_h

#include "query_engine.h"
#include <TargetConditionals.h>
#if TARGET_OS_SIMULATOR
#include "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h"
#else
#include "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h"
#endif
Comment on lines +5 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing platform guard will break non-Apple builds.

<TargetConditionals.h> is an Apple-only header. This code compiles on Android where this header doesn't exist, causing a build failure. Additionally, cpp/react-native-prisma.cpp:5 still uses #include "query_engine.h" directly, creating inconsistency.

Wrap the Apple-specific conditional in a platform check:

🐛 Proposed fix to add platform guard
+#if defined(__APPLE__)
 `#include` <TargetConditionals.h>
 `#if` TARGET_OS_SIMULATOR
 `#include` "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h"
 `#else`
 `#include` "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h"
 `#endif`
+#else
+#include "query_engine.h"
+#endif
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <TargetConditionals.h>
#if TARGET_OS_SIMULATOR
#include "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h"
#else
#include "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h"
#endif
`#if` defined(__APPLE__)
`#include` <TargetConditionals.h>
`#if` TARGET_OS_SIMULATOR
`#include` "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h"
`#else`
`#include` "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h"
`#endif`
`#else`
`#include` "query_engine.h"
`#endif`
🧰 Tools
🪛 Clang (14.0.6)

[error] 5-5: 'TargetConditionals.h' file not found

(clang-diagnostic-error)

#include <jsi/jsi.h>
#include <memory>
#include <string>
Expand Down
60 changes: 54 additions & 6 deletions cpp/react-native-prisma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "macros.h"
#include "query_engine.h"
#include "utils.h"
#include <cstdlib>
#include <iostream>
#include <unordered_map>

Expand All @@ -18,6 +19,12 @@ std::unordered_map<std::string, std::shared_ptr<QueryEngineHostObject>>
engine_map;
ThreadPool thread_pool;

static void free_engine_string(const char *value) {
if (value != nullptr) {
free(const_cast<char *>(value));
}
}

// Pure C function that is used by Rust to call the log callback
extern void log_callback(const char *id, const char *msg) {
if (engine_map.count(id)) {
Expand Down Expand Up @@ -81,7 +88,7 @@ void install_cxx(jsi::Runtime &rt,
}

auto log_callback_fn = [&rt, js_log_callback](std::string msg) {
call_invoker->invokeAsync([&rt, msg, &js_log_callback] {
call_invoker->invokeAsync([&rt, msg, js_log_callback] {
js_log_callback->asObject(rt).asFunction(rt).call(
rt, jsi::String::createFromUtf8(rt, msg));
});
Expand Down Expand Up @@ -130,7 +137,7 @@ void install_cxx(jsi::Runtime &rt,
auto resolve = std::make_shared<jsi::Value>(rt, args[0]);
auto reject = std::make_shared<jsi::Value>(rt, args[1]);

auto task = [&rt, &queryEngineHostObject, body = std::move(body),
auto task = [&rt, queryEngineHostObject, body = std::move(body),
trace = std::move(trace), tx_id = std::move(tx_id), resolve,
reject]() {
const char *response;
Expand All @@ -147,8 +154,10 @@ void install_cxx(jsi::Runtime &rt,
call_invoker->invokeAsync([&rt, response = std::move(response),
error_ptr, resolve, reject]() {
if (error_ptr == nullptr) {
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
resolve->asObject(rt).asFunction(rt).call(
rt, jsi::String::createFromUtf8(rt, response));
rt, std::move(js_response));
} else {
auto errCtr = rt.global().getPropertyAsFunction(rt, "Error");
std::string error_message(error_ptr);
Expand All @@ -170,6 +179,38 @@ void install_cxx(jsi::Runtime &rt,
return promise;
});

auto execute_sync = HOSTFN("executeSync", 4) {
std::shared_ptr<QueryEngineHostObject> queryEngineHostObject =
args[0].asObject(rt).asHostObject<QueryEngineHostObject>(rt);
std::string body = args[1].asString(rt).utf8(rt);
std::string trace = args[2].asString(rt).utf8(rt);
std::string tx_id;
if (count > 3 && args[3].isString()) {
tx_id = args[3].asString(rt).utf8(rt);
}

const char *response = nullptr;
char *error_ptr = nullptr;

if (!tx_id.empty()) {
response = prisma_query(queryEngineHostObject->engine, body.c_str(),
trace.c_str(), tx_id.c_str(), &error_ptr);
} else {
response = prisma_query(queryEngineHostObject->engine, body.c_str(),
trace.c_str(), nullptr, &error_ptr);
}

if (error_ptr == nullptr) {
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
Comment on lines +203 to +206

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard null query responses before creating a JSI string.

If prisma_query returns nullptr without setting error_ptr, createFromUtf8(rt, response) can crash. Mirror the transaction handlers’ null-response guard before conversion.

Proposed fix
     if (error_ptr == nullptr) {
+      if (response == nullptr) {
+        throw std::runtime_error("prisma engine did not return a query response");
+      }
       auto js_response = jsi::String::createFromUtf8(rt, response);
       free_engine_string(response);
       return js_response;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error_ptr == nullptr) {
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
if (error_ptr == nullptr) {
if (response == nullptr) {
throw std::runtime_error("prisma engine did not return a query response");
}
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/react-native-prisma.cpp` around lines 203 - 206, The query response path
in the Prisma JSI bridge can crash when prisma_query returns a null response
without setting error_ptr. Update the response handling in
react-native-prisma.cpp near the code that creates the JSI string to mirror the
transaction handler null-response guard: check response for null before calling
jsi::String::createFromUtf8(rt, response), and return a proper error/empty
result path instead of converting a null pointer.

}

std::string error_message(error_ptr);
free(error_ptr);
throw std::runtime_error(error_message);
});

auto start_transaction = HOSTFN("startTransaction", 3) {
std::shared_ptr<QueryEngineHostObject> queryEngineHostObject =
args[0].asObject(rt).asHostObject<QueryEngineHostObject>(rt);
Expand All @@ -183,7 +224,9 @@ void install_cxx(jsi::Runtime &rt,
throw std::runtime_error("prisma engine did not start transaction");
}

return jsi::String::createFromUtf8(rt, std::string(response));
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
});

auto commit_transaction = HOSTFN("commitTransaction", 3) {
Expand All @@ -199,7 +242,9 @@ void install_cxx(jsi::Runtime &rt,
throw std::runtime_error("prisma engine did not commit transaction");
}

return jsi::String::createFromUtf8(rt, std::string(response));
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
});

auto rollback_transaction = HOSTFN("rollbackTransaction", 3) {
Expand All @@ -215,7 +260,9 @@ void install_cxx(jsi::Runtime &rt,
throw std::runtime_error("prisma engine did not rollback transaction");
}

return jsi::String::createFromUtf8(rt, std::string(response));
auto js_response = jsi::String::createFromUtf8(rt, response);
free_engine_string(response);
return js_response;
});

auto disconnect = HOSTFN("disconnect", 2) {
Expand Down Expand Up @@ -253,6 +300,7 @@ void install_cxx(jsi::Runtime &rt,
module.setProperty(rt, "create", std::move(create));
module.setProperty(rt, "connect", std::move(connect));
module.setProperty(rt, "execute", std::move(execute));
module.setProperty(rt, "executeSync", std::move(execute_sync));
module.setProperty(rt, "startTransaction", std::move(start_transaction));
module.setProperty(rt, "commitTransaction", std::move(commit_transaction));
module.setProperty(rt, "rollbackTransaction",
Expand Down
115 changes: 91 additions & 24 deletions ios/Prisma.mm
Original file line number Diff line number Diff line change
@@ -1,52 +1,119 @@
#import "Prisma.h"
#import <jsi/jsi.h>
#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <dispatch/dispatch.h>
#import <ReactCommon/RCTTurboModule.h>
#import <UIKit/UIKit.h>
#import <iostream>

#ifdef RCT_NEW_ARCH_ENABLED
#import "RNPrismaSpecJSI.h"
#endif

// Forward declare runtimeExecutor to silence selector warnings on older headers
@interface RCTBridge (RuntimeExecutorForwardDecl)
- (facebook::react::RuntimeExecutor)runtimeExecutor;
@end

static NSString *PrismaLibraryPath() {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true);
return [paths objectAtIndex:0];
}

static NSString *PrismaMigrationsPath() {
auto bundleURL = NSBundle.mainBundle.bundleURL;
auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"];
return [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""];
}

static inline void InstallInRuntime(facebook::jsi::Runtime &runtime,
std::shared_ptr<facebook::react::CallInvoker> callInvoker) {
NSString *libraryPath = PrismaLibraryPath();
NSString *migrationsPath = PrismaMigrationsPath();
prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrationsPath UTF8String]);
}

static RCTBridge *ResolveBridge(id<RCTBridgeModule> module) {
RCTBridge *bridge = nil;
if ([module respondsToSelector:@selector(bridge)]) {
bridge = ((id<RCTBridgeModule>)module).bridge;
}
if (bridge == nil) {
bridge = [RCTBridge currentBridge];
}
return bridge;
}

@implementation Prisma

@synthesize bridge=_bridge;

RCT_EXPORT_MODULE()

// Old-arch sync install for classic bridge
#if !RCT_NEW_ARCH_ENABLED
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install)
{
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)_bridge;
if (cxxBridge == nil) {
return @false;
#if DEBUG
std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl;
std::cout << "▲ Library Path:\n" << [PrismaLibraryPath() UTF8String] << std::endl;
std::cout << "▲ Migrations Path:\n" << [PrismaMigrationsPath() UTF8String] << std::endl;
#endif

BOOL ok = NO;
auto okPtr = &ok;
RCTBridge *bridge = ResolveBridge(self);

// Try new-arch path first if runtimeExecutor is available
if (bridge && [bridge respondsToSelector:@selector(runtimeExecutor)]) {
facebook::react::RuntimeExecutor executor = RCTRuntimeExecutorFromBridge(bridge);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
executor([&](facebook::jsi::Runtime &runtime) {
InstallInRuntime(runtime, bridge.jsCallInvoker);
*okPtr = YES;
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return @(ok);
}
Comment on lines +70 to 80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential deadlock if InstallInRuntime throws.

If InstallInRuntime throws an exception inside the executor block, dispatch_semaphore_signal is never called, causing dispatch_semaphore_wait to block indefinitely.

🐛 Proposed fix using try-catch
     if (bridge && [bridge respondsToSelector:`@selector`(runtimeExecutor)]) {
         facebook::react::RuntimeExecutor executor = RCTRuntimeExecutorFromBridge(bridge);
         dispatch_semaphore_t sema = dispatch_semaphore_create(0);
+        __block NSException *caughtException = nil;
         executor([&](facebook::jsi::Runtime &runtime) {
-          InstallInRuntime(runtime, bridge.jsCallInvoker);
-          *okPtr = YES;
+          `@try` {
+            InstallInRuntime(runtime, bridge.jsCallInvoker);
+            *okPtr = YES;
+          } `@catch` (NSException *e) {
+            caughtException = e;
+          }
           dispatch_semaphore_signal(sema);
         });
         dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
+        if (caughtException) {
+          `@throw` caughtException;
+        }
         return @(ok);
     }

Note: C++ exceptions from JSI may need different handling (e.g., std::exception_ptr). Consider using a C++ try-catch block instead if JSI throws C++ exceptions.


auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime;
if (jsiRuntime == nil) {

// Fallback to old-arch bridge access
RCTBridge *legacyBridge = _bridge ?: bridge;
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)legacyBridge;
if (cxxBridge == nil || cxxBridge.runtime == nil) {
NSLog(@"[Prisma] no runtime available to install cxx");
return @false;
}

auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime;
auto &runtime = *jsiRuntime;
auto callInvoker = _bridge.jsCallInvoker;

// get migrations folder
auto bundleURL = NSBundle.mainBundle.bundleURL;
auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"];
auto migrations_path = [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""];

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true);
NSString *libraryPath = [paths objectAtIndex:0];

#if DEBUG
std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl;
std::cout << "▲ Library Path:\n" << [libraryPath UTF8String] << std::endl;
std::cout << "▲ Migrations Path:\n" << [migrations_path UTF8String] << std::endl;
#endif
auto callInvoker = _bridge ? _bridge.jsCallInvoker : legacyBridge.jsCallInvoker;

prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrations_path UTF8String]);
return nil;
InstallInRuntime(runtime, callInvoker);
return @true;
}
#else
// Required by NativePrismaSpec protocol in new-arch; TurboModule path uses C++ spec below.
- (void)install {}
#endif

#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativePrismaSpecJSI>(params);
class PrismaCxxTurboModule final : public facebook::react::NativePrismaCxxSpec<PrismaCxxTurboModule> {
public:
explicit PrismaCxxTurboModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
: NativePrismaCxxSpec(std::move(jsInvoker)) {}

void install(facebook::jsi::Runtime &rt) {
InstallInRuntime(rt, this->jsInvoker_);
}
};

return std::make_shared<PrismaCxxTurboModule>(params.jsInvoker);
}
#endif

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"!**/__mocks__",
"!**/.*",
"copy-migrations.sh",
"scripts/patch-prisma-runtime.cjs",
"react-native-prisma.gradle",
"plugin",
"app.plugin.js",
Expand All @@ -42,6 +43,7 @@
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"prepare": "yarn download-engine && bob build",
"postinstall": "node ./scripts/patch-prisma-runtime.cjs",
"ios:full": "yarn qe:sim && cd example && yarn ios",
"clang": "clang-format -i ./cpp/*.cpp ./cpp/*.h && git add .",
"ios": "cd example && yarn ios",
Expand Down
Loading