react-native-litert-lm 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +153 -135
  2. package/android/build.gradle +12 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +276 -62
  5. package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +19 -2
  6. package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
  7. package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +105 -0
  8. package/ios/HybridLiteRTLM.swift +1344 -0
  9. package/ios/Tests/HybridLiteRTLMTests.swift +113 -0
  10. package/lib/__mocks__/react-native-nitro-modules.d.ts +65 -0
  11. package/lib/__mocks__/react-native-nitro-modules.js +60 -0
  12. package/lib/__tests__/hooks.test.d.ts +1 -0
  13. package/lib/__tests__/hooks.test.js +124 -0
  14. package/lib/__tests__/memoryTracker.test.d.ts +1 -0
  15. package/lib/__tests__/memoryTracker.test.js +74 -0
  16. package/lib/__tests__/modelFactory.test.d.ts +1 -0
  17. package/lib/__tests__/modelFactory.test.js +68 -0
  18. package/lib/hooks.js +27 -3
  19. package/lib/index.d.ts +6 -2
  20. package/lib/index.js +8 -8
  21. package/lib/modelFactory.js +82 -63
  22. package/lib/specs/LiteRTLM.nitro.d.ts +87 -2
  23. package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +2 -2
  24. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +94 -9
  25. package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +5 -1
  26. package/nitrogen/generated/android/c++/JLLMConfig.hpp +40 -3
  27. package/nitrogen/generated/android/c++/JMultimodalPart.hpp +74 -0
  28. package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
  29. package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +23 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +28 -2
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +46 -3
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +19 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +15 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +66 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
  38. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +57 -1
  39. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +414 -3
  40. package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +41 -3
  41. package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +4 -6
  42. package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +10 -0
  43. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
  44. package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +240 -0
  45. package/nitrogen/generated/ios/swift/Backend.swift +44 -0
  46. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  47. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  48. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  49. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  50. package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
  51. package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
  52. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +71 -0
  53. package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +431 -0
  54. package/nitrogen/generated/ios/swift/LLMConfig.swift +203 -0
  55. package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
  56. package/nitrogen/generated/ios/swift/Message.swift +34 -0
  57. package/nitrogen/generated/ios/swift/MultimodalPart.swift +83 -0
  58. package/nitrogen/generated/ios/swift/PartType.swift +44 -0
  59. package/nitrogen/generated/ios/swift/Role.swift +44 -0
  60. package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
  61. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +4 -0
  62. package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +9 -2
  63. package/nitrogen/generated/shared/c++/LLMConfig.hpp +22 -2
  64. package/nitrogen/generated/shared/c++/MultimodalPart.hpp +99 -0
  65. package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
  66. package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
  67. package/package.json +22 -11
  68. package/react-native-litert-lm.podspec +17 -19
  69. package/scripts/download-ios-frameworks.sh +17 -50
  70. package/scripts/framework-source.js +46 -0
  71. package/scripts/postinstall.js +40 -18
  72. package/src/__mocks__/react-native-nitro-modules.ts +58 -0
  73. package/src/__tests__/hooks.test.ts +153 -0
  74. package/src/__tests__/memoryTracker.test.ts +87 -0
  75. package/src/__tests__/modelFactory.test.ts +96 -0
  76. package/src/hooks.ts +29 -7
  77. package/src/index.ts +7 -10
  78. package/src/modelFactory.ts +104 -80
  79. package/src/specs/LiteRTLM.nitro.ts +106 -2
  80. package/cpp/HybridLiteRTLM.cpp +0 -939
  81. package/cpp/HybridLiteRTLM.hpp +0 -169
  82. package/cpp/IOSDownloadHelper.h +0 -24
  83. package/ios/IOSDownloadHelper.mm +0 -129
  84. package/scripts/build-ios-engine.sh +0 -302
  85. package/scripts/stubs/cxx_bridge_stubs.cc +0 -224
  86. package/scripts/stubs/gemma_model_constraint_provider.cc +0 -46
  87. package/scripts/stubs/llguidance_stubs.c +0 -101
  88. package/src/templates.ts +0 -105
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "react-native-litert-lm",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
4
4
  "litertLm": {
5
- "version": "0.10.2",
6
- "androidMavenVersion": "0.10.2",
7
- "iosGitTag": "v0.10.2"
5
+ "version": "0.12.0",
6
+ "androidMavenVersion": "0.12.0",
7
+ "iosGitTag": "v0.12.0"
8
8
  },
9
9
  "description": "High-performance LLM inference for React Native using LiteRT-LM. Optimized for Gemma 4 and other on-device language models.",
10
10
  "license": "MIT",
@@ -43,14 +43,14 @@
43
43
  "android/src",
44
44
  "android/build.gradle",
45
45
  "android/CMakeLists.txt",
46
- "ios/*.mm",
47
- "ios/*.m",
46
+ "ios/**/*.swift",
47
+ "ios/**/*.mm",
48
+ "ios/**/*.m",
48
49
  "cpp",
49
50
  "nitrogen/generated",
50
51
  "scripts/postinstall.js",
52
+ "scripts/framework-source.js",
51
53
  "scripts/download-ios-frameworks.sh",
52
- "scripts/build-ios-engine.sh",
53
- "scripts/stubs",
54
54
  "react-native.config.js",
55
55
  "react-native-litert-lm.podspec",
56
56
  "app.plugin.js",
@@ -64,11 +64,14 @@
64
64
  "postinstall": "node scripts/postinstall.js",
65
65
  "build": "tsc",
66
66
  "typecheck": "tsc --noEmit",
67
+ "test": "jest",
67
68
  "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
68
- "prepack": "npm run build",
69
+ "prepack": "npx nitrogen && npm run build",
69
70
  "specs": "npx nitrogen",
70
71
  "clean": "rm -rf lib android/build ios/build ios/Frameworks nitrogen/generated",
71
72
  "download-frameworks": "scripts/download-ios-frameworks.sh",
73
+ "check-framework-release": "node scripts/check-framework-release.js",
74
+ "prepublishOnly": "node scripts/check-framework-release.js",
72
75
  "android": "expo run:android",
73
76
  "android:clean": "cd android && ./gradlew clean",
74
77
  "ios": "expo run:ios",
@@ -76,16 +79,24 @@
76
79
  },
77
80
  "devDependencies": {
78
81
  "@expo/config-plugins": "~55.0.0",
82
+ "@types/jest": "^30.0.0",
79
83
  "@types/react": "~19.2.10",
80
- "react-native-nitro-modules": "^0.35.4",
84
+ "@types/react-test-renderer": "^19.1.0",
85
+ "jest": "^30.4.2",
86
+ "react": "^19.2.6",
87
+ "react-native": "^0.85.3",
88
+ "react-native-nitro-modules": "^0.35.9",
89
+ "react-test-renderer": "^19.2.6",
81
90
  "release-it": "^19.2.4",
91
+ "ts-jest": "^29.4.10",
92
+ "ts-node": "^10.9.2",
82
93
  "typescript": "^5.0.0"
83
94
  },
84
95
  "peerDependencies": {
85
96
  "expo": ">=55.0.0",
86
97
  "react": "*",
87
98
  "react-native": "*",
88
- "react-native-nitro-modules": "^0.35.4"
99
+ "react-native-nitro-modules": "^0.35.9"
89
100
  },
90
101
  "peerDependenciesMeta": {
91
102
  "expo": {
@@ -9,36 +9,30 @@ Pod::Spec.new do |s|
9
9
  s.homepage = package["homepage"]
10
10
  s.license = package["license"]
11
11
  s.authors = package["author"]
12
- s.platforms = { :ios => "15.0" }
12
+ s.platforms = { :ios => "15.1" }
13
+ s.module_name = "LiteRTLM"
13
14
  s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
14
15
 
15
- s.swift_version = '5.0'
16
+ s.swift_version = '5.9'
16
17
 
17
18
  s.source_files = [
18
- # Implementation (C++)
19
- "cpp/**/*.{hpp,cpp,h}",
20
- # Autolinking (Objective-C++)
21
- "ios/**/*.{m,mm}",
22
- # Nitrogen generated iOS bridge
19
+ # Swift, Objective-C/C++ implementation & autolinking
20
+ "ios/*.{swift,m,mm}",
21
+ # Nitrogen generated iOS bridge & shared C++ interfaces
23
22
  "nitrogen/generated/ios/**/*.{mm,swift}",
23
+ "nitrogen/generated/shared/c++/**/*.{hpp,cpp}",
24
24
  ]
25
25
 
26
- # Exclude Android-only JNI files from iOS build
27
- s.exclude_files = [
28
- "cpp/cpp-adapter.cpp",
29
- ]
30
-
31
- # Prebuilt LiteRT-LM C engine (static library built from Bazel //c:engine target).
32
- # Downloaded from GitHub releases by postinstall.js, or built locally via:
33
- # scripts/build-ios-engine.sh
34
- s.vendored_frameworks = 'ios/Frameworks/LiteRTLM.xcframework'
26
+ # Prebuilt LiteRT-LM C engine (xcframework). Not shipped in the npm tarball;
27
+ # fetched on install by scripts/postinstall.js (asset/tag defined in
28
+ # scripts/framework-source.js). Manual/upstream fallback:
29
+ # scripts/download-ios-frameworks.sh.
30
+ s.vendored_frameworks = 'ios/Frameworks/CLiteRTLM.xcframework'
35
31
 
36
32
  s.pod_target_xcconfig = {
37
33
  'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20',
38
- 'CLANG_CXX_LIBRARY' => 'libc++',
34
+ 'SWIFT_VERSION' => '5.9',
39
35
  'HEADER_SEARCH_PATHS' => [
40
- '"$(PODS_TARGET_SRCROOT)/cpp"',
41
- '"$(PODS_TARGET_SRCROOT)/cpp/include"',
42
36
  '"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++"',
43
37
  '"$(PODS_TARGET_SRCROOT)/nitrogen/generated/ios"',
44
38
  ].join(' '),
@@ -59,6 +53,10 @@ Pod::Spec.new do |s|
59
53
  s.frameworks = ['Metal', 'MetalPerformanceShaders', 'Accelerate', 'CoreML', 'CoreGraphics']
60
54
  s.libraries = ['c++']
61
55
 
56
+ s.test_spec 'Tests' do |test_spec|
57
+ test_spec.source_files = 'ios/Tests/**/*.{swift}'
58
+ end
59
+
62
60
  install_modules_dependencies(s)
63
61
  end
64
62
 
@@ -1,72 +1,39 @@
1
1
  #!/bin/bash
2
2
  # download-ios-frameworks.sh
3
- #
4
- # Downloads prebuilt LiteRT-LM iOS static engine from this project's GitHub
5
- # releases. If the prebuilt asset is not available, falls back to building
6
- # from source via Bazel (see build-ios-engine.sh).
7
- #
8
- # The XCFramework contains a static library compiled from the LiteRT-LM
9
- # C engine (//c:engine Bazel target) for both device (arm64) and simulator
10
- # (sim_arm64).
11
- #
12
- # Usage:
13
- # ./scripts/download-ios-frameworks.sh
3
+ # Downloads the official prebuilt LiteRT-LM iOS framework (CLiteRTLM.xcframework)
4
+ # directly from the google-ai-edge/LiteRT-LM releases.
14
5
 
15
6
  set -euo pipefail
16
7
 
17
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
9
  PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
- OUTPUT_DIR="$PROJECT_ROOT/ios/Frameworks"
20
- C_API_HEADER_DIR="$PROJECT_ROOT/cpp/include"
21
10
 
22
- LITERT_LM_VERSION="$(node -e "console.log(require('$PROJECT_ROOT/package.json').litertLm.iosGitTag)")"
23
- GITHUB_RAW="https://github.com/google-ai-edge/LiteRT-LM/raw/${LITERT_LM_VERSION}"
24
-
25
- # Read version from package.json
26
- PACKAGE_VERSION=$(node -e "console.log(require('$PROJECT_ROOT/package.json').version)" 2>/dev/null || echo "0.0.0")
27
- GITHUB_REPO="hung-yueh/react-native-litert-lm"
28
- ASSET_NAME="LiteRTLM-ios-frameworks.zip"
11
+ # Resolve the asset URL + output dir from the shared single source of truth
12
+ # (scripts/framework-source.js) so this manual path can't drift from postinstall.
13
+ RELEASE_URL="$(node -e "console.log(require('$SCRIPT_DIR/framework-source').ASSET_URL)")"
14
+ OUTPUT_DIR="$(node -e "console.log(require('$SCRIPT_DIR/framework-source').FRAMEWORKS_DIR)")"
29
15
 
30
16
  # Skip if already present
31
- if [ -d "$OUTPUT_DIR" ] && [ "$(find "$OUTPUT_DIR" -name "*.xcframework" 2>/dev/null | wc -l)" -gt 0 ]; then
32
- echo "[LiteRT-LM] iOS frameworks already present at ios/Frameworks/, skipping."
17
+ if [ -d "$OUTPUT_DIR/CLiteRTLM.xcframework" ]; then
18
+ echo "[LiteRT-LM] iOS CLiteRTLM.xcframework already present, skipping download."
33
19
  exit 0
34
20
  fi
35
21
 
36
- # ---- Ensure C API header is vendored --------------------------------------
37
- echo "[LiteRT-LM] Vendoring C API header..."
38
- mkdir -p "$C_API_HEADER_DIR"
39
- curl -fsSL -o "$C_API_HEADER_DIR/litert_lm_engine.h" \
40
- "${GITHUB_RAW}/c/engine.h" 2>/dev/null || true
41
-
42
- # ---- Try downloading prebuilt from our GitHub releases --------------------
43
- RELEASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${ASSET_NAME}"
44
-
45
- echo "[LiteRT-LM] Attempting to download prebuilt iOS engine from:"
22
+ echo "[LiteRT-LM] Downloading prebuilt iOS engine from Google's release:"
46
23
  echo " ${RELEASE_URL}"
47
24
 
25
+ mkdir -p "$OUTPUT_DIR"
48
26
  TMP_ZIP="$PROJECT_ROOT/.ios-frameworks-tmp.zip"
49
- if curl -fsSL -o "$TMP_ZIP" "$RELEASE_URL" 2>/dev/null; then
50
- echo "[LiteRT-LM] Download successful, extracting..."
51
- rm -rf "$OUTPUT_DIR"
52
- mkdir -p "$OUTPUT_DIR"
27
+
28
+ if curl -fsSL -o "$TMP_ZIP" "$RELEASE_URL"; then
29
+ echo "[LiteRT-LM] Download successful, extracting to $OUTPUT_DIR..."
30
+ rm -rf "$OUTPUT_DIR/CLiteRTLM.xcframework"
53
31
  unzip -o -q "$TMP_ZIP" -d "$OUTPUT_DIR"
54
32
  rm -f "$TMP_ZIP"
55
- echo "[LiteRT-LM] ✅ iOS frameworks installed from prebuilt release."
33
+ echo "[LiteRT-LM] ✅ iOS frameworks successfully installed."
56
34
  exit 0
57
- fi
58
-
59
- rm -f "$TMP_ZIP"
60
- echo "[LiteRT-LM] Prebuilt not available for v${PACKAGE_VERSION}."
61
-
62
- # ---- Fall back to building from source ------------------------------------
63
- echo "[LiteRT-LM] Falling back to building from source via Bazel..."
64
- echo ""
65
-
66
- if [ -x "$SCRIPT_DIR/build-ios-engine.sh" ]; then
67
- exec "$SCRIPT_DIR/build-ios-engine.sh"
68
35
  else
69
- echo "Error: build-ios-engine.sh not found or not executable."
70
- echo "Run manually: ./scripts/build-ios-engine.sh"
36
+ rm -f "$TMP_ZIP"
37
+ echo "Error: Failed to download LiteRT-LM iOS framework from ${RELEASE_URL}"
71
38
  exit 1
72
39
  fi
@@ -0,0 +1,46 @@
1
+ /**
2
+ * framework-source.js
3
+ *
4
+ * Single source of truth for the prebuilt iOS framework artifact, shared by
5
+ * scripts/postinstall.js, scripts/download-ios-frameworks.sh, and
6
+ * scripts/check-framework-release.js, so the asset name, tag, and URL can never
7
+ * drift between the install path and the release-time guardrail.
8
+ *
9
+ * The artifact is Google's canonical LiteRT-LM release (NOT a per-version
10
+ * re-host of this package), pinned to the LiteRT-LM **engine** version
11
+ * (`litertLm.iosGitTag` in package.json), NOT this wrapper's npm version. So:
12
+ * - patch releases of this wrapper reuse the same framework (no re-upload),
13
+ * - there is no per-release asset for the maintainer to forget to upload,
14
+ * - the guardrail checks the exact URL consumers fetch.
15
+ *
16
+ * Override the host/asset (e.g. to point at a private mirror) via env vars:
17
+ * LITERT_FRAMEWORK_REPO (default: google-ai-edge/LiteRT-LM)
18
+ * LITERT_FRAMEWORK_ASSET (default: CLiteRTLM.xcframework.zip)
19
+ */
20
+
21
+ const path = require('path');
22
+ const packageJson = require('../package.json');
23
+
24
+ /** GitHub repo that hosts the framework release asset. */
25
+ const GITHUB_REPO = process.env.LITERT_FRAMEWORK_REPO || 'google-ai-edge/LiteRT-LM';
26
+
27
+ /** Release asset filename. Must match the file on the GitHub release. */
28
+ const ASSET_NAME = process.env.LITERT_FRAMEWORK_ASSET || 'CLiteRTLM.xcframework.zip';
29
+
30
+ /** Release tag the asset lives under — the LiteRT-LM engine git tag, e.g. "v0.12.0". */
31
+ const FRAMEWORK_TAG = packageJson.litertLm.iosGitTag;
32
+
33
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
34
+ const FRAMEWORKS_DIR = path.join(PACKAGE_ROOT, 'ios', 'Frameworks');
35
+
36
+ /** Fully-resolved download URL for the framework zip. */
37
+ const ASSET_URL = `https://github.com/${GITHUB_REPO}/releases/download/${FRAMEWORK_TAG}/${ASSET_NAME}`;
38
+
39
+ module.exports = {
40
+ GITHUB_REPO,
41
+ ASSET_NAME,
42
+ FRAMEWORK_TAG,
43
+ PACKAGE_ROOT,
44
+ FRAMEWORKS_DIR,
45
+ ASSET_URL,
46
+ };
@@ -5,10 +5,15 @@
5
5
  * Downloads prebuilt LiteRT-LM iOS frameworks from this package's GitHub
6
6
  * releases when consumers run `npm install react-native-litert-lm`.
7
7
  *
8
+ * The framework is intentionally NOT shipped inside the npm tarball (it is
9
+ * ~40MB and irrelevant to Android-only consumers). The asset name, tag, and
10
+ * URL come from scripts/framework-source.js so they stay in lockstep with the
11
+ * release-time guardrail (scripts/check-framework-release.js).
12
+ *
8
13
  * Skips download if:
9
14
  * - Not on macOS (iOS builds require macOS)
10
15
  * - Frameworks already exist
11
- * - CI environment with SKIP_IOS_FRAMEWORK_DOWNLOAD=1
16
+ * - SKIP_IOS_FRAMEWORK_DOWNLOAD=1 (e.g. Android-only / CI builds)
12
17
  */
13
18
 
14
19
  const { execSync } = require('child_process');
@@ -16,14 +21,12 @@ const fs = require('fs');
16
21
  const path = require('path');
17
22
  const https = require('https');
18
23
 
19
- const PACKAGE_JSON = require('../package.json');
20
- const PACKAGE_VERSION = PACKAGE_JSON.version;
21
- const GITHUB_REPO = 'hung-yueh/react-native-litert-lm';
22
- const ASSET_NAME = 'LiteRTLM-ios-frameworks.zip';
24
+ const { ASSET_URL, FRAMEWORKS_DIR, FRAMEWORK_TAG } = require('./framework-source');
23
25
 
24
- const SCRIPT_DIR = __dirname;
25
- const PACKAGE_ROOT = path.resolve(SCRIPT_DIR, '..');
26
- const FRAMEWORKS_DIR = path.join(PACKAGE_ROOT, 'ios', 'Frameworks');
26
+ /** ZIP local-file-header magic ("PK\x03\x04"). Guards against truncated downloads / HTML error pages. */
27
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
28
+ /** A valid framework zip is well over this; anything smaller is certainly an error page. */
29
+ const MIN_VALID_BYTES = 1024 * 1024; // 1 MB
27
30
 
28
31
  function log(msg) {
29
32
  console.log(`[react-native-litert-lm] ${msg}`);
@@ -74,25 +77,46 @@ function downloadFile(url, destPath, maxRedirects = 5) {
74
77
  const file = fs.createWriteStream(destPath);
75
78
  res.pipe(file);
76
79
  file.on('finish', () => {
77
- file.close();
78
- resolve();
80
+ file.close(resolve);
79
81
  });
80
82
  file.on('error', reject);
81
83
  }).on('error', reject);
82
84
  });
83
85
  }
84
86
 
87
+ /**
88
+ * Verify the downloaded file is a non-trivial ZIP before we trust it.
89
+ * Catches GitHub HTML error pages and truncated downloads that would
90
+ * otherwise fail cryptically at `unzip` or, worse, link a corrupt framework.
91
+ */
92
+ function assertValidZip(zipPath) {
93
+ const { size } = fs.statSync(zipPath);
94
+ if (size < MIN_VALID_BYTES) {
95
+ throw new Error(`downloaded asset is only ${size} bytes — expected a multi-MB framework zip (likely an error page or truncated download)`);
96
+ }
97
+
98
+ const header = Buffer.alloc(4);
99
+ const fd = fs.openSync(zipPath, 'r');
100
+ try {
101
+ fs.readSync(fd, header, 0, 4, 0);
102
+ } finally {
103
+ fs.closeSync(fd);
104
+ }
105
+ if (!header.equals(ZIP_MAGIC)) {
106
+ throw new Error('downloaded asset is not a valid ZIP (bad magic bytes)');
107
+ }
108
+ }
109
+
85
110
  async function main() {
86
111
  if (shouldSkip()) return;
87
112
 
88
- const releaseUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${ASSET_NAME}`;
89
-
90
- log(`Downloading iOS frameworks from: ${releaseUrl}`);
113
+ log(`Downloading iOS frameworks (engine ${FRAMEWORK_TAG}) from: ${ASSET_URL}`);
91
114
 
92
- const tmpZip = path.join(PACKAGE_ROOT, '.ios-frameworks-tmp.zip');
115
+ const tmpZip = path.join(path.dirname(FRAMEWORKS_DIR), '.ios-frameworks-tmp.zip');
93
116
 
94
117
  try {
95
- await downloadFile(releaseUrl, tmpZip);
118
+ await downloadFile(ASSET_URL, tmpZip);
119
+ assertValidZip(tmpZip);
96
120
 
97
121
  // Extract
98
122
  fs.mkdirSync(FRAMEWORKS_DIR, { recursive: true });
@@ -108,11 +132,9 @@ async function main() {
108
132
 
109
133
  log(`Error: Could not download iOS frameworks: ${err.message}`);
110
134
  log('iOS builds will not work until frameworks are available.');
111
- log('Run: ./scripts/download-ios-frameworks.sh to download manually,');
112
- log(' or: ./scripts/build-ios-engine.sh to build from source.');
135
+ log('Run: ./scripts/download-ios-frameworks.sh to download manually.');
113
136
 
114
137
  // Fail fast on macOS so users discover the problem now, not at Xcode link time.
115
- // Skip SKIP_IOS_FRAMEWORK_DOWNLOAD is already checked above.
116
138
  if (process.platform === 'darwin') {
117
139
  log('Set SKIP_IOS_FRAMEWORK_DOWNLOAD=1 to suppress this error (e.g. Android-only builds).');
118
140
  process.exit(1);
@@ -0,0 +1,58 @@
1
+ export const mockLiteRTLM = {
2
+ isReady: jest.fn(() => false),
3
+ loadModel: jest.fn().mockResolvedValue(undefined),
4
+ sendMessage: jest.fn().mockResolvedValue("Mock response"),
5
+ sendMessageWithImage: jest.fn().mockResolvedValue("Mock vision response"),
6
+ downloadModel: jest.fn(async (url, fileName, onProgress) => {
7
+ onProgress?.(1.0);
8
+ return "/mock/path/model.litertlm";
9
+ }),
10
+ deleteModel: jest.fn().mockResolvedValue(undefined),
11
+ sendMessageWithAudio: jest.fn().mockResolvedValue("Mock audio response"),
12
+ sendMultimodalMessage: jest.fn().mockResolvedValue("Mock multimodal response"),
13
+ sendMessageAsync: jest.fn((msg, onToken) => {
14
+ onToken("Mock ", false);
15
+ onToken("token", true);
16
+ return Promise.resolve();
17
+ }),
18
+ sendMessageWithImageAsync: jest.fn((msg, imagePath, onToken) => {
19
+ onToken("Mock vision ", false);
20
+ onToken("token", true);
21
+ return Promise.resolve();
22
+ }),
23
+ sendMessageWithAudioAsync: jest.fn((msg, audioPath, onToken) => {
24
+ onToken("Mock audio ", false);
25
+ onToken("token", true);
26
+ return Promise.resolve();
27
+ }),
28
+ getHistory: jest.fn(() => []),
29
+ resetConversation: jest.fn(),
30
+ getStats: jest.fn(() => ({
31
+ promptTokens: 10,
32
+ completionTokens: 20,
33
+ totalTokens: 30,
34
+ timeToFirstToken: 5,
35
+ totalTime: 50,
36
+ tokensPerSecond: 400,
37
+ })),
38
+ countTokens: jest.fn(() => -1),
39
+ getMemoryUsage: jest.fn(() => ({
40
+ nativeHeapBytes: 1000000,
41
+ residentBytes: 2000000,
42
+ availableMemoryBytes: 4000000,
43
+ isLowMemory: false,
44
+ })),
45
+ close: jest.fn(),
46
+ };
47
+
48
+ export const NitroModules = {
49
+ createHybridObject: jest.fn((name: string) => {
50
+ if (name === "LiteRTLM") {
51
+ return mockLiteRTLM;
52
+ }
53
+ throw new Error(`Mock not implemented for hybrid object: ${name}`);
54
+ }),
55
+ createNativeArrayBuffer: jest.fn((size: number) => {
56
+ return new ArrayBuffer(size);
57
+ }),
58
+ };
@@ -0,0 +1,153 @@
1
+ // Configure React act environment
2
+ (global as any).IS_REACT_ACT_ENVIRONMENT = true;
3
+
4
+ import { useModel } from '../hooks';
5
+ import { mockLiteRTLM } from '../__mocks__/react-native-nitro-modules';
6
+ import TestRenderer from 'react-test-renderer';
7
+ import React from 'react';
8
+
9
+ // Mock react-native
10
+ jest.mock('react-native', () => ({
11
+ Platform: {
12
+ OS: 'ios',
13
+ select: jest.fn((dict) => dict.ios),
14
+ },
15
+ }));
16
+
17
+ // Helper to render and test hooks using react-test-renderer
18
+ function renderHook<P, R>(callback: (props: P) => R, initialProps?: P) {
19
+ let result = { current: null as unknown as R };
20
+
21
+ const TestComponent = ({ props }: { props: P }) => {
22
+ result.current = callback(props);
23
+ return null;
24
+ };
25
+
26
+ let renderer: TestRenderer.ReactTestRenderer;
27
+ TestRenderer.act(() => {
28
+ renderer = TestRenderer.create(React.createElement(TestComponent, { props: initialProps as P }));
29
+ });
30
+
31
+ const rerender = (newProps: P) => {
32
+ TestRenderer.act(() => {
33
+ renderer.update(React.createElement(TestComponent, { props: newProps }));
34
+ });
35
+ };
36
+
37
+ const unmount = () => {
38
+ TestRenderer.act(() => {
39
+ renderer.unmount();
40
+ });
41
+ };
42
+
43
+ return { result, rerender, unmount };
44
+ }
45
+
46
+ describe('useModel React Hook Unit Tests', () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+
51
+ it('should initialize with correct default state and call loadModel automatically when autoLoad is true', async () => {
52
+ let hookResult: any;
53
+
54
+ await TestRenderer.act(async () => {
55
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
56
+ });
57
+
58
+ expect(hookResult.result.current.isReady).toBe(true);
59
+ expect(hookResult.result.current.isGenerating).toBe(false);
60
+ expect(hookResult.result.current.downloadProgress).toBe(1); // loadModel completed
61
+ expect(hookResult.result.current.error).toBeNull();
62
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should not call loadModel automatically when autoLoad is false', async () => {
66
+ let hookResult: any;
67
+
68
+ await TestRenderer.act(async () => {
69
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
70
+ });
71
+
72
+ expect(hookResult.result.current.isReady).toBe(false);
73
+ expect(mockLiteRTLM.loadModel).not.toHaveBeenCalled();
74
+
75
+ // Call load manually
76
+ await TestRenderer.act(async () => {
77
+ await hookResult.result.current.load();
78
+ });
79
+
80
+ expect(hookResult.result.current.isReady).toBe(true);
81
+ expect(mockLiteRTLM.loadModel).toHaveBeenCalled();
82
+ });
83
+
84
+ it('should handle model load failure gracefully', async () => {
85
+ mockLiteRTLM.loadModel.mockRejectedValueOnce(new Error("Model load failed"));
86
+ let hookResult: any;
87
+
88
+ await TestRenderer.act(async () => {
89
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
90
+ });
91
+
92
+ expect(hookResult.result.current.isReady).toBe(false);
93
+ expect(hookResult.result.current.error).toBe("Model load failed");
94
+ });
95
+
96
+ it('should generate text successfully and trigger memory summary update', async () => {
97
+ let hookResult: any;
98
+
99
+ await TestRenderer.act(async () => {
100
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', {
101
+ autoLoad: true,
102
+ enableMemoryTracking: true
103
+ }));
104
+ });
105
+
106
+ let response = "";
107
+ await TestRenderer.act(async () => {
108
+ response = await hookResult.result.current.generate("Test prompt");
109
+ });
110
+
111
+ expect(response).toBe("Mock token");
112
+ expect(mockLiteRTLM.sendMessageAsync).toHaveBeenCalled();
113
+ expect(hookResult.result.current.memorySummary).toBeDefined();
114
+ });
115
+
116
+ it('should reset conversation correctly', async () => {
117
+ let hookResult: any;
118
+
119
+ await TestRenderer.act(async () => {
120
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
121
+ });
122
+
123
+ hookResult.result.current.reset();
124
+ expect(mockLiteRTLM.resetConversation).toHaveBeenCalled();
125
+ });
126
+
127
+ it('should delete model file correctly', async () => {
128
+ let hookResult: any;
129
+
130
+ await TestRenderer.act(async () => {
131
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: true }));
132
+ });
133
+
134
+ await TestRenderer.act(async () => {
135
+ await hookResult.result.current.deleteModel();
136
+ });
137
+
138
+ expect(mockLiteRTLM.deleteModel).toHaveBeenCalledWith('model.litertlm');
139
+ expect(hookResult.result.current.isReady).toBe(false);
140
+ expect(hookResult.result.current.downloadProgress).toBe(0);
141
+ });
142
+
143
+ it('should call close on unmount', async () => {
144
+ let hookResult: any;
145
+
146
+ await TestRenderer.act(async () => {
147
+ hookResult = renderHook(() => useModel('https://example.com/model.litertlm', { autoLoad: false }));
148
+ });
149
+
150
+ hookResult.unmount();
151
+ expect(mockLiteRTLM.close).toHaveBeenCalled();
152
+ });
153
+ });