From 5642b2e137eebe030a1d5413f70b29a5f66e991d Mon Sep 17 00:00:00 2001 From: Cuc Dan Mihai Date: Fri, 17 Jul 2026 11:58:47 +0300 Subject: [PATCH 1/3] fix(resource-fetcher): avoid `this` in static `fs` field initializer (#1310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `ResourceFetcher.fs.readAsString` is an async arrow inside a **static class-field initializer** and relies on `this` binding to the class. Because the package resolves through the `"react-native": "src/index"` entry, Metro compiles this TypeScript source with the **consuming app's** Babel config. Under `@react-native/babel-preset` with `unstable_transformProfile: 'hermes-stable'` (or `'hermes-canary'`), the async transform hoists the arrow's `this` capture to module scope: ```js var _this = this; // module scope! class ResourceFetcher { static fs = { readAsString: /* ... */ _this.getAdapter().readAsString(path) /* ... */ }; } ``` `_this.getAdapter` is `undefined`, so **every model load fails** with `TypeError: undefined is not a function` inside `LLMController.load`, right after the download finishes — on both iOS and Android. The `default` transform profile compiles it correctly, which is why this only bites apps that opt into the hermes profiles. Fix: reference the class by name instead of `this`. One line, no behavior change. ### Introduces a breaking change? - [ ] Yes - [x] No ### Type of change - [x] Bug fix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [ ] Other (chores, tests, code style improvements etc.) ### Tested on - [x] iOS - [x] Android ### Testing instructions 1. In a bare RN app (RN 0.86, New Architecture) set `unstable_transformProfile: 'hermes-stable'` on `@react-native/babel-preset` in `babel.config.js`. 2. Load any LLM, e.g. `useLLM({ model: GEMMA4_E2B })`, on a physical device. 3. Without this patch: load fails with `TypeError: undefined is not a function` after the download completes. With it: the model loads and generates. Reproducible without a device: run `@babel/core` on `src/utils/ResourceFetcher.ts` with the preset above and inspect the output — the `var _this = this` hoist lands at module scope. Verified end-to-end on a physical iPhone 16 with `GEMMA4_E2B` (MLX backend): download → load → generation all work with this patch (applied via `yarn patch` against 0.9.2). ### Screenshots N/A ### Related issues N/A ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [x] My changes generate no new warnings ### Additional notes A repo-wide sweep found no other `this` usage inside static-field initializers, so this is the only affected site. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Cuc Dan Mihai Co-authored-by: Claude Fable 5 (cherry picked from commit a0bf85bf152516f5c151dd014abeb42b806bd474) --- packages/react-native-executorch/src/utils/ResourceFetcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native-executorch/src/utils/ResourceFetcher.ts b/packages/react-native-executorch/src/utils/ResourceFetcher.ts index 7b31f65584..56b9923fb3 100644 --- a/packages/react-native-executorch/src/utils/ResourceFetcher.ts +++ b/packages/react-native-executorch/src/utils/ResourceFetcher.ts @@ -138,8 +138,10 @@ export class ResourceFetcher { * @remarks * **REQUIRED**: Used internally for reading configuration files (e.g., tokenizer configs). */ + // Reference the class by name: `this` inside a static-field arrow gets + // rebound to module scope by Babel under RN's hermes transform profiles. readAsString: async (path: string) => { - return this.getAdapter().readAsString(path); + return ResourceFetcher.getAdapter().readAsString(path); }, }; } From 8a4bc7151612b7ab5f76419929d0dd9330250d0d Mon Sep 17 00:00:00 2001 From: Injun Choi <80089617+injunchoi98@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:22:55 +0900 Subject: [PATCH 2/3] fix(android): forward ExecuTorch consumer ProGuard rules (#1325) ## Description Fixes an Android release crash caused by R8 removing ExecuTorch classes that are accessed during JNI initialization. at minified release builds without the consumer rules, the app crashes during `ETInstaller` initialization: ```text java.lang.ClassNotFoundException: org.pytorch.executorch.Module at java.lang.System.loadLibrary(...) at com.swmansion.rnexecutorch.ETInstaller.(...) ``` This PR ports the upstream rules and registers them through `consumerProguardFiles` The first two rules ared covered with RN's own consumer rules, but are intentionally retained to keep the upstream ExecuTorch rule block intact. The rule that fixes the observed `Module` crash is: ```proguard -keepclasseswithmembers class org.pytorch.executorch.** { native ; } ``` ### Introduces a breaking change? - [ ] Yes - [x] No ### Type of change - [x] Bug fix (change which fixes an issue) - [ ] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [ ] Other (chores, tests, code style improvements etc.) ### Tested on - [ ] iOS - [x] Android ### Testing instructions 1. Enable R8/ProGuard for the release build. 2. Build the minified release APK 3. Verify that JNI-accessed classes retain their names in mapping.txt ### Screenshots ### Related issues ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly - [x] My changes generate no new warnings ### Additional notes Android-only; iOS does not use R8. tested on a android physical device(galaxy s20 FE) The upstream ExecuTorch rules are copied without semantic changes. (cherry picked from commit 189c24fe20a6445b130d92be08e869e065721c4a) --- .../android/build.gradle | 1 + .../android/consumer-proguard-rules.pro | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/react-native-executorch/android/consumer-proguard-rules.pro diff --git a/packages/react-native-executorch/android/build.gradle b/packages/react-native-executorch/android/build.gradle index 5b1cfd2973..be4a2f5373 100644 --- a/packages/react-native-executorch/android/build.gradle +++ b/packages/react-native-executorch/android/build.gradle @@ -115,6 +115,7 @@ android { defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + consumerProguardFiles "consumer-proguard-rules.pro" externalNativeBuild { cmake { abiFilters (*reactNativeArchitectures()) diff --git a/packages/react-native-executorch/android/consumer-proguard-rules.pro b/packages/react-native-executorch/android/consumer-proguard-rules.pro new file mode 100644 index 0000000000..de12e0bdec --- /dev/null +++ b/packages/react-native-executorch/android/consumer-proguard-rules.pro @@ -0,0 +1,51 @@ +# Ported from ExecuTorch's official Android consumer rules: +# https://github.com/pytorch/executorch/blob/904c667007aed15e9a3bb8278aa271b479450c85/extension/android/executorch_android/consumer-proguard-rules.pro + +# ExecuTorch Android AAR — Consumer ProGuard/R8 Rules +# +# These rules are automatically applied to any app that depends on the +# ExecuTorch AAR. They prevent R8/ProGuard from stripping classes and +# methods that are called from native (JNI) code. + +# Keep ExecuTorch classes and members annotated with @DoNotStrip. +# Scoped to org.pytorch.executorch to avoid affecting unrelated libraries. +-keep @com.facebook.jni.annotations.DoNotStrip class org.pytorch.executorch.** { *; } +-keepclassmembers class org.pytorch.executorch.** { + @com.facebook.jni.annotations.DoNotStrip *; +} + +# Keep all native methods across ExecuTorch packages. +# Use -keepclasseswithmembers (not -keepclasseswithmembernames) to prevent +# both shrinking and obfuscation of JNI entry points. +-keepclasseswithmembers class org.pytorch.executorch.** { + native ; +} + +# Keep HybridData fields (accessed by fbjni via reflection). +-keepclassmembers class org.pytorch.executorch.** { + com.facebook.jni.HybridData *; +} + +# Keep ExecutorchRuntimeException and its factory/subclasses. +# These are instantiated from JNI via jni_helper.cpp. +-keep class org.pytorch.executorch.ExecutorchRuntimeException { *; } +-keep class org.pytorch.executorch.ExecutorchRuntimeException$* { *; } + +# Keep EValue (fields and type codes accessed from native code). +-keep class org.pytorch.executorch.EValue { *; } + +# Keep Tensor and its inner classes. The Tensor class has methods and fields +# accessed from JNI (dtypeJniCode, shape, getRawDataBuffer, nativeNewTensor). +-keep class org.pytorch.executorch.Tensor { *; } +-keep class org.pytorch.executorch.Tensor$* { *; } + +# Keep LlmCallback interface methods (invoked from native code). +-keep interface org.pytorch.executorch.extension.llm.LlmCallback { *; } + +# Keep AsrCallback interface methods (invoked from native code). +-keep interface org.pytorch.executorch.extension.asr.AsrCallback { *; } + +# --- react-native-executorch rules (not from upstream) --- + +# RNE's own JNI entry point, resolved by literal descriptor from C++. +-keep class com.swmansion.rnexecutorch.ETInstaller { *; } From e7d618d868fe0b7ecffb086afedf6f39f83ba46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Tue, 28 Jul 2026 14:52:38 +0200 Subject: [PATCH 3/3] chore: Release v0.9.3 --- packages/react-native-executorch/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-executorch/package.json b/packages/react-native-executorch/package.json index ab7a8e8e9d..ca81fea5fa 100644 --- a/packages/react-native-executorch/package.json +++ b/packages/react-native-executorch/package.json @@ -1,6 +1,6 @@ { "name": "react-native-executorch", - "version": "0.9.2", + "version": "0.9.3", "description": "An easy way to run AI models in React Native with ExecuTorch", "source": "./src/index.ts", "main": "./lib/module/index.js",